先认识它,再驾驭它
大家好,我是柒八九。
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},同时还会生成相关的作用域- 生成字节码(介于
AST和机器代码的中间代码)
- 与特定类型的机器代码无关
- 解释器(
ignition),按照顺序解释执行字节码,并输出执行结果。
通过V8将js转换为字节码然后经过解释器执行输出结果的方式执行JS,有一个弊端就是,如果在浏览器中再次打开相同的页面,当页面中的 JavaScript 文件没有被修改,再次编译之后的二进制代码也会保持不变,意味着编译这一步浪费了 CPU 资源。
为了,更好的利用CPU资源,V8采用JIT(Just In Time)技术提升效率:而是混合编译执行和解释执行这两种手段。
JIT引入了两个编译器
- 基线编译器
- 如果一段代码变成了
warm,那么JIT就把它送到编译器去编译,并且把编译结果存储起来。
- 优化编译器
- 如果一个代码段变得
very hot,监视器会把它发送到优化编译器中。生成一个更快速和高效的代码版本出来,并且存储之。 优化编译器最成功一个特点叫做{类型特化|Type specialization}- 因为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_profilesource 你的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可以设定两种不同的编译目标
WebAssemblyasm.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编程
全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。

















