
前端小魔女
2023/03/19阅读:28主题:蔷薇紫
浏览器第四种语言-WebAssembly
❝先认识它,再驾驭它
❞
大家好,我是「柒八九」。
ChatGPT
知道吧!现在最新的new Bing
中已经接入了AI
功能。

而能够实现上述让人欲罢不能的功能。OenAI
是永远绕不开的话题。
而OpenAI
是一家人工智能研究机构,他们在 2020
年推出了一款基于 WebAssembly
的 AI 模型推理引擎,名为 Microscope
。Microscope
可以在现代浏览器中运行,提供了高效的 AI 模型推理能力。
既然,AI
的模型,我们搞不定;那么WebAssembly
这种更贴近前端开发者的技术,我们还是可以「窥探一番」的。
好了,天不早了,我们开始今天WebAssembly基础知识
的探索之旅。

你能所学到的知识点
❝❞
WebAssembly
是个啥? 「推荐阅读指数」⭐️⭐️⭐️⭐️⭐️使用 Emscripten
写一个属于你的wasm
「推荐阅读指数」⭐️⭐️⭐️⭐️⭐️胶水代码 「推荐阅读指数」⭐️⭐️⭐️⭐️ 编译目标及编译流程 「推荐阅读指数」⭐️⭐️⭐️
WebAssembly是个啥?
❝
WebAssembly
(简称Wasm
)是一种可以在现代Web
浏览器中运行的「低级字节码」。❞
它是一种可移植、大小合理和加载速度快的格式,适用于 Web
上的各种应用程序。
WebAssembly
是一种新的编程语言,并不是JavaScript
的替代品。相反,它是一种补充,可以与现有的Web
技术一起使用。WebAssembly 可以被编译成 JavaScript,也可以直接在浏览器中运行。
❝
WebAssembly
也是新一代Web 虚拟机标准,可以让用「各种语言」编写的代码都能以接近原生的速度在Web
中运行❞
C/C++
代码可以通过Emscripten
工具链编译为wasm
二进制文件,进而导入网页中供js
调用Rust
语言更是内置了对WebAssembly
的支持
WebAssembly 诞生背景
在目前的Web
应用中,JavaScript
属于「一家独大」的地位。但是,由于JS
是「弱类型语言」,变量类型不是固定的,在「使用变量前需要判断其类型,无疑增加了运算的复杂度,降低了执行效率」。
为了提高JS
的效率,Mozila
的工程师创建了Emscripten
项目,尝试通过LLVM
工具链将C/C++
语言编写的程序转译为JS
代码,并在此过程中创建了JS子集
(asm.js
)。
❝❞
asm.js
仅包含可以预判变量类型的数值运算,有效地避免了JS
弱类型变量语法带来的执行效率低的痛点。
asm.js
显著的提升了JS
效率,获得了主流浏览器厂商的支持。并且,各大厂商决定采用「二进制格式」来表示asm.js
模块(减少模块体积,提升模块加载和解析速度),最终演化出WebAssembly
技术。
Web的第四种语言

见图知意,WebAssembly
已经被内置到浏览器中了。同时,.wasm
可以直接运行在浏览器中。作为网页开发的「第四大」主力开发语言。
在浏览器控制台中,直接打印就可以看到WebAssembly
构造函数。
WebAssembly解决的痛点
下面,我们来简单复现一下,V8
是如何处理JS
的。

-
V8
接收到要执行的JS 源代码
-
源代码
对V8
来说只是「一堆字符串」,V8
并不能直接理解这段字符串的含义
-
-
V8
结构化这段字符串,生成了抽象语法树,同时还会生成相关的「作用域」 -
生成字节码(介于 AST
和机器代码
的中间代码)-
「与特定类型的机器代码无关」
-
-
「解释器」( ignition
),「按照顺序解释执行字节码」,并输出执行结果。
通过V8
将js
转换为字节码
然后经过解释器
执行输出结果的方式执行JS
,有一个弊端就是,如果在浏览器中「再次打开相同的页面」,当页面中的 JavaScript
文件没有被修改,再次编译之后的二进制代码也会保持不变,意味着编译这一步「浪费了 CPU 资源」。
为了,更好的利用CPU资源,V8采用「JIT」(Just In Time)技术提升效率:而是「混合编译执行和解释执行这两种手段」。

「JIT」引入了两个编译器
-
「基线编译器」 -
如果一段代码变成了 warm
,那么JIT
就把它送到「编译器」去编译,并且把编译结果存储起来。
-
-
「优化编译器」 -
如果一个代码段变得 very hot
,监视器
会把它发送到「优化编译器」中。生成一个更快速和高效的代码版本出来,并且存储之。 -
优化编译器
最成功一个特点叫做类型特化 -
因为JS是「动态类型语言」,在代码运行过程中,如果是多形态的(即调用的过程中,类型不断变化),则会为操作所调用的每一个类型组合生成一个桩。 -
-
「如果存在多形态的情况,无形中就会增加了JS编译执行的时间」。
-
我们可以从几个方面来描述一下,WebAssembly
是如何解决现有问题的。
角度 | 方式 |
---|---|
「汇编角度」 | WebAssembly 提供了一种更接近于机器码的中间表示形式,使得代码在浏览器中的执行速度更快。它允许开发者编写高性能的代码,同时保持「跨平台兼容性」。 |
「v8中的JIT」 | JavaScript 在浏览器中通过JIT (Just-In-Time)编译器执行,但JIT编译过程需要时间,WebAssembly的二进制格式可以更快地解码和执行。这意味着WebAssembly 可以减少浏览器在解析和优化代码方面的开销,从而提高性能。 |
「类型特化角度」 | JavaScript 是一种「动态类型语言」,这意味着在运行时需要进行类型检查和转换。WebAssembly则是静态类型的,这使得它在编译和执行时可以避免这些类型检查和转换的开销。此外,静态类型还有助于提高代码的可读性和可维护性。 |
「JVM角度」 | WebAssembly 提供了一种独立于语言和平台的虚拟机,类似于JVM ,但专为Web而设计,使得各种编程语言都可以在浏览器中高效运行。 |
WebAssembly 优点
角度 | 原因 |
---|---|
性能 | WebAssembly 代码执行速度接近原生代码,因为它是为快速解码和执行而设计的。 |
安全 | WebAssembly 在沙箱环境中运行,保护系统资源免受恶意代码的侵害。 |
可移植性 | WebAssembly 模块可以在任何支持的浏览器和平台上运行,无需修改。 |
与 JavaScript 互操作 |
WebAssembly 可以与 JavaScript 代码无缝协作,使得开发者可以在性能关键部分使用 WebAssembly ,而在其他部分使用 JavaScript 。 |
语言支持 | WebAssembly 支持多种编程语言,如 C、C++、Rust 等,使得开发者可以使用熟悉的语言编写高性能 Web 应用。 |
WebAssembly应用
WebAssembly
目前已经得到了许多公司的支持和应用,以下是一些落地项目和成就的例子:
-
「Unity Technologies」: Unity
是一家游戏引擎和游戏开发工具提供商,他们在 2018 年推出了一款基于WebAssembly
的游戏引擎,名为 "Unity 2018.2"。这款引擎可以在现代浏览器中运行,提供了与原生应用程序相同的性能和功能。 -
「Fastly」: Fastly
是一家内容传递网络(CDN
)提供商,他们在 2019 年推出了一款名为 "Lucet" 的WebAssembly
运行时。Lucet
可以在云端和边缘设备上运行WebAssembly
代码,提供了比传统服务器更高的性能和可扩展性。 -
「Figma」: Figma
是一款基于 Web 的界面设计工具,他们在 2020 年推出了一款名为 "FigJam" 的新产品,其中使用了WebAssembly
技术。FigJam
可以在浏览器中实时协作,并提供了高效的图形处理能力。 -
「OpenAI」: OpenAI
是一家人工智能研究机构,他们在2020
年推出了一款基于WebAssembly
的 AI 模型推理引擎,名为Microscope
。Microscope
可以在现代浏览器中运行,提供了高效的 AI 模型推理能力。(最近名声大噪的-ChatGPT4
你是否了解呢。神器一般的存在)
使用 Emscripten 写一个属于你的 wasm
Emscripten
是用C/C++
语言开发WebAssembly
应用的标准工具,是WebAssembly
宿主接口事实上的标准之一。
安装 Emscripten
Emscripten
包含了将C/C++
代码编译为WebAssembly
所需的「完整工具集」(LLVM/Node.js/Python/Java
等),不依赖于任何其他的编译器环境。
可以使用emsdk
命令行工具安装Emscripten
。
下载最新版的Python
emsdk
是一组基于Python
的脚本。我们可以在Python 官网下载并安装最新版的Python
。
$ python --version // 3.11.2
下载emsdk
Python
准备就绪后,下载emsdk
工具包。
// 下载emsdk
$ git clone https://github.com/emscripten-core/emsdk.git
安装并激活Emscripten
在控制台切换到emsdk
所在目录。
针对MacOS
或者Linux
用户,可以按照下面的代码进行配置处理。
$ cd emsdk
运行以下emsdk
命令从GitHub
获取最新工具,并将其设置为「活动状态」
# 获取最新版本的emsdk (第一次clone项目的时候,忽略此操作)
git pull
# 下载按照最新的SDK工具
./emsdk install latest
# 针对当前用户,将最新的SDK设置为“激活状态”
./emsdk activate latest
# 激活当前终端中的路径和其他环境变量
source ./emsdk_env.sh
「上面的命令中的输出,这里就不贴图了」。
对于Windows
用户,按照Emscripten
的方法基本一致。执行代码的区别是使用emsdk.bat
代替emsdk
,使用emsdk_env.bat
代替source ./emsdk_env.sh
。
emsdk.bat update
# 下载按照最新的SDK工具
emsdk.bat install latest
# 针对当前用户,将最新的SDK设置为“激活状态”
emsdk.bat activate latest
# 激活当前终端中的路径和其他环境变量
emsdk_env.bat
❝Note: 安装及激活
❞Emscripten
「只需要执行一次」,然后在新建的控制台中设置一次环境变量,既可使用Emscripten
核心命令emcc
emcc 全局安装
如果想要在全局范围内,使用emcc
。可以使用如下步骤:
❝❞
vim ~/.bash_profile
source 你的emsdk安装路径/emsdk_env.sh
校验安装
Emscripten
安装/激活且设置环境变量后,可以通过emcc -v
查看版本信息。
> emcc -v
// 以下是控制台输出日志:
emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 3.1.33 (c1927f22708aa9a26a5956bab61de083e8d3e463)
clang version 17.0.0 (https://github.com/llvm/llvm-project 671eeece457f6a5da7489f7b48f7afae55327b8b)
Target: wasm32-unknown-emscripten
Thread model: posix
InstalledDir: /Users/PersonWorkSpace/WasmWorkSpace/emsdk/upstream/bin
编码环节
又到了,我们接触新语言的环节 -- 写一个hello,world
程序。
生成.wasm文件
「由于我们是用Emscripten
作为案例演示,所以我们用C
语言来写代码」
新建一个名为hello.cc
的C
源文件。
#include <stdio.h>
int main(){
printf("hello,world!\n");
return 0;
}
进入控制台,执行以下命令进行编译:
emcc hello.cc
在hello.cc
所在的目录下得到两个文件

-
a.out.wasm
-
该文件为 C
源文件编译后形成的WebAssembly
汇编文件
-
-
a.out.js
-
是 Emscripten
生成的胶水代码,其中「包含了Emscripten
的运行环境和.wasm
文件的封装」 -
导入 a.out.js
既可自动完成.wasm
文件的载入/编译/实例化、运行时初始化等工作。
-
我们还可以使用-o
选项指定emcc
的输出文件
emcc hello.cc -o hell.js
在hello.cc
所在的目录下得到两个文件 分别为 hello.wasm
和hello.js
代码引用
与原生代码不同,C/C++
代码被编译为WebAssembly
后是无法直接运行的。我们需要将其导入网页,通过浏览器来执行。
在HTML中引用JS
我们在vscode
中使用emmet
直接搞一个最简单的html
。然后引入我们刚才生成的hello.js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Emscripten</title>
</head>
<body>
<script src="./hello.js"></script>
</body>
</html>
然后,还有一点需要注意,WebAssembly
是需要通过「网页发布」后才可以运行。
这里,我们用node
写了一个最简单的服务器。
const http = require("http"),
fs = require("fs"),
path = require("path"),
url = require("url");
// 获取当前目录
let root = path.resolve();
// 创建服务器
let sever = http.createServer(function(request,response){
let pathname = url.parse(request.url).pathname;
let filepath = path.join(root,pathname);
// 获取文件状态
fs.stat(filepath,function(err,stats){
if(err){
// 发送404响应
response.writeHead(404);
response.end("404 Not Found.");
}else{
// 发送200响应
response.writeHead(200);
// response是一个writeStream对象,fs读取html后,可以用pipe方法直接写入
fs.createReadStream(filepath).pipe(response);
}
});
});
sever.listen(7899);
console.log('Sever is running at http://127.0.0.1:7899/');
这样,我们就可以通过http://127.0.0.1:7899/hello.html
访问到刚才生成的hello.js
了。
然后,项目的结构如下:

在http://127.0.0.1:7899/hello.html
的控制台,就能看到hello,world
的输出结果。

在Node 环境下使用
WebAssembly
程序也可以在Node.js 8+
版本中运行。

在Vite中使用
如果大家对Vite
熟悉的话,它是支持直接将.wasm
文件引入到项目中的。
这里就直接拿来主义了哈。
利用vite-plugin-wasm
插件进行引入处理
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import wasm from "vite-plugin-wasm";
export default defineConfig({
plugins: [react(),wasm()],
})
预编译的 .wasm
文件可以通过 ?init
来导入。默认导出一个初始化函数,返回值为所导出 wasm
实例对象的 Promise
:
import init from './example.wasm?init'
init().then((instance) => {
instance.exports.test()
})
但是呢,如果你把上面利用Emscripten
生成的hello.wasm
会报错。
TypeError: WebAssembly.Instance():
Import #0 module="wasi_unstable" error: module is not an object or function
使用 emscripten
构建的 wasm
模块,推荐的做法是让 emscripten
生成 JS
来实现这些 API
,并为你加载模块。
在网页中直接使用wasm
使用 WebAssembly
可以在网页中运行更快、更强大的应用程序。要在网页中使用 WebAssembly
,需要遵循以下步骤:
-
编写 WebAssembly
模块,可以使用C/C++、Rust
等语言编写。 -
将 WebAssembly
模块编译为wasm
格式。 -
在 JavaScript
中加载wasm
模块。 -
在 JavaScript
中调用wasm
模块中的函数。
下面是一个简单的例子,演示如何在网页中使用 WebAssembly
:
我们改造一下刚才的hello.cc
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int main() {
int a = 2;
int b = 3;
int result = add(a, b);
printf("The sum of %d and %d is %d\\n", a, b, result);
return 0;
}
使用Emscripten
编译器将该代码编译为WebAssembly
格式。以下是一个示例命令:
emcc hello.c -o hello.wasm -s WASM=1 -s EXPORTED_FUNCTIONS="['_main', '_add']"
该命令将_main
和_add
函数作为可导出的函数,以便在WebAssembly
模块中调用它们。然后,您可以将生成的WASM
文件嵌入到HTML
文件中,并使用JavaScript
代码调用它们。
// 加载 wasm 模块
fetch('hello.wasm')
.then(response => response.arrayBuffer())
.then(bytes => WebAssembly.instantiate(bytes))
.then(results => {
// 调用 wasm 模块中的函数
const { add } = results.instance.exports;
console.log(add(1, 2)); // 输出 3
});
在上面的例子中,我们
-
首先使用 fetch
函数加载wasm
模块, -
然后使用 WebAssembly.instantiate
函数将其实例化。 -
最后,我们可以通过 results.instance.exports
对象访问wasm
模块中的函数,并在JavaScript
中调用它们。
胶水代码
Emscripten
在编译时,生成了大量的JS
胶水代码。
我们通过VScode
打开hello.js
发现,大多数的操作都围绕「全局对象」Module
展开。该对象正是Emscripten
程序运行的核心所在。
我们可以通过vscode
快捷键Ctrl+K+0
将所有函数折叠起来。这样方便查看顶层函数的定义。
WebAssembly汇编模块载入
WebAssembly
汇编模块(即.wasm
)的载入是在instantiateAsync
中完成的。

上述代码就做了几件事
-
尝试使用 WebAssembly.instantiateStreaming()
创建wasm
模块的实例 -
如果流式创建失败,改用 WebAssembly.instantiate()
方法创建实例 -
成功实例化后返回值交由 receiveInstance
方法处理

在receiveInstance
中执行了下面的指令:
Module['asm'] = exports;
❝意思就是将
❞wasm
模块实例的「导出对象」传给Module
的子对象asm
。
异步加载
WebAssembly
实例是通过WebAssembly.instantiateStreaming()
或WebAssembly.instantiate()
方法创建的,而这两个方法均为「异步调用」,这就意味着.js
加载完成时,Emscripten
的运行时并未准备就绪。
就会出现在载入hello.js
后,立即调用Module._main()
会报错。
解决这一问题需要建立一种运行时准备就绪的通知机制。我们可以使用onRuntimeInitialized
回调。
<body>
<script>
Module ={};
Module.onRuntimeInitialized = function(){
Module._main();
}
</script>
<script src="./hello.js"></script>
</body>
基本思路就是在Module
初始化前,向Module
中注入一个名为onRuntimeInitialized
的方法,当Emscripten
的运行时准备就绪时,将会回调该方法。
在hello.js
中的run()
中调用了onRuntimeInitialized

编译目标及编译流程
Emscripten
可以设定两种不同的编译目标
-
WebAssembly
-
asm.js
编译目标的选择
以asm.js
为编译目标时,C/C++
代码被编译为.js
文件;以WebAssembly
为编译目标时,C/C++
代码被编译为.wasm
文件及对应的.js
胶水代码文件。
二者在实际应用中「主要区别」在于模块加载的同步还是异步:
-
以 asm.js
为编译目标时,由于C/C++
代码被完全转换成asm.js
(JS子集),因此认为模块是同步加载的 -
以 WebAssembly
为编译目标时,由于WebAssembly
的实例化方法本身是异步指令,因为认为模块是异步加载的
❝在兼容性允许的情况下,应尽量以
❞WebAssembly
为编译目标
编译流程

C/C++
代码通过Clang
编译为LLVM
字节码,然后根据不同的目标编译为asm.js
或wasm
。
后记
「分享是一种态度」。
参考地址
-
emscripten.org -
WebAssembly -
面向WebAssembly编程
「全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。」

作者介绍

前端小魔女
微信公众号:前端柒八九