先认识它,再驾驭它
大家好,我是柒八九。
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_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编程
全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。