❝先认识它,再驾驭它❞
大家好,我是「柒八九」。
ChatGPT
知道吧!现在最新的new Bing
中已经接入了AI
功能。
而能够实现上述让人欲罢不能的功能。OenAI
是永远绕不开的话题。
而OpenAI
是一家人工智能研究机构,他们在 2020
年推出了一款基于 WebAssembly
的 AI 模型推理引擎,名为 Microscope
。Microscope
可以在现代浏览器中运行,提供了高效的 AI 模型推理能力。
既然,AI
的模型,我们搞不定;那么WebAssembly
这种更贴近前端开发者的技术,我们还是可以「窥探一番」的。
好了,天不早了,我们开始今天WebAssembly基础知识
的探索之旅。
❝
WebAssembly
是个啥? 「推荐阅读指数」⭐️⭐️⭐️⭐️⭐️Emscripten
写一个属于你的 wasm
「推荐阅读指数」⭐️⭐️⭐️⭐️⭐️❞
❝
WebAssembly
(简称Wasm
)是一种可以在现代Web
浏览器中运行的「低级字节码」。
Web
上的各种应用程序。❞
WebAssembly
是一种新的编程语言,并不是JavaScript
的替代品。相反,它是一种补充,可以与现有的Web
技术一起使用。WebAssembly 可以被编译成 JavaScript,也可以直接在浏览器中运行。
❝
WebAssembly
也是新一代Web 虚拟机标准,可以让用「各种语言」编写的代码都能以接近原生的速度在Web
中运行
C/C++
代码可以通过Emscripten
工具链编译为wasm
二进制文件,进而导入网页中供js
调用Rust
语言更是内置了对WebAssembly
的支持❞
在目前的Web
应用中,JavaScript
属于「一家独大」的地位。但是,由于JS
是「弱类型语言」,变量类型不是固定的,在「使用变量前需要判断其类型,无疑增加了运算的复杂度,降低了执行效率」。
为了提高JS
的效率,Mozila
的工程师创建了Emscripten
项目,尝试通过LLVM
工具链将C/C++
语言编写的程序转译为JS
代码,并在此过程中创建了JS子集
(asm.js
)。
❝
asm.js
仅包含可以预判变量类型的数值运算,有效地避免了JS
弱类型变量语法带来的执行效率低的痛点。 ❞
asm.js
显著的提升了JS
效率,获得了主流浏览器厂商的支持。并且,各大厂商决定采用「二进制格式」来表示asm.js
模块(减少模块体积,提升模块加载和解析速度),最终演化出WebAssembly
技术。
一图胜千言
见图知意,WebAssembly
已经被内置到浏览器中了。同时,.wasm
可以直接运行在浏览器中。作为网页开发的「第四大」主力开发语言。
在浏览器控制台中,直接打印就可以看到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我们可以从几个方面来描述一下,WebAssembly
是如何解决现有问题的。
角度 | 方式 |
---|---|
「汇编角度」 | WebAssembly提供了一种更接近于机器码的中间表示形式,使得代码在浏览器中的执行速度更快。它允许开发者编写高性能的代码,同时保持「跨平台兼容性」。 |
「v8中的JIT」 | JavaScript在浏览器中通过JIT(Just-In-Time)编译器执行,但JIT编译过程需要时间,WebAssembly的二进制格式可以更快地解码和执行。这意味着WebAssembly可以减少浏览器在解析和优化代码方面的开销,从而提高性能。 |
「类型特化角度」 | JavaScript是一种「动态类型语言」,这意味着在运行时需要进行类型检查和转换。WebAssembly则是静态类型的,这使得它在编译和执行时可以避免这些类型检查和转换的开销。此外,静态类型还有助于提高代码的可读性和可维护性。 |
「JVM角度」 | WebAssembly提供了一种独立于语言和平台的虚拟机,类似于JVM,但专为Web而设计,使得各种编程语言都可以在浏览器中高效运行。 |
角度 | 原因 |
---|---|
性能 | WebAssembly 代码执行速度接近原生代码,因为它是为快速解码和执行而设计的。 |
安全 | WebAssembly 在沙箱环境中运行,保护系统资源免受恶意代码的侵害。 |
可移植性 | WebAssembly 模块可以在任何支持的浏览器和平台上运行,无需修改。 |
与 JavaScript 互操作 | WebAssembly 可以与 JavaScript 代码无缝协作,使得开发者可以在性能关键部分使用 WebAssembly,而在其他部分使用 JavaScript。 |
语言支持 | WebAssembly 支持多种编程语言,如 C、C++、Rust 等,使得开发者可以使用熟悉的语言编写高性能 Web 应用。 |
WebAssembly
目前已经得到了许多公司的支持和应用,以下是一些落地项目和成就的例子:
Unity
是一家游戏引擎和游戏开发工具提供商,他们在 2018 年推出了一款基于 WebAssembly
的游戏引擎,名为 "Unity 2018.2"。这款引擎可以在现代浏览器中运行,提供了与原生应用程序相同的性能和功能。Fastly
是一家内容传递网络(CDN
)提供商,他们在 2019 年推出了一款名为 "Lucet" 的 WebAssembly
运行时。Lucet
可以在云端和边缘设备上运行 WebAssembly
代码,提供了比传统服务器更高的性能和可扩展性。Figma
是一款基于 Web 的界面设计工具,他们在 2020 年推出了一款名为 "FigJam" 的新产品,其中使用了 WebAssembly
技术。FigJam
可以在浏览器中实时协作,并提供了高效的图形处理能力。OpenAI
是一家人工智能研究机构,他们在 2020
年推出了一款基于 WebAssembly
的 AI 模型推理引擎,名为 Microscope
。Microscope
可以在现代浏览器中运行,提供了高效的 AI 模型推理能力。(最近名声大噪的-ChatGPT4
你是否了解呢。神器一般的存在)Emscripten
是用C/C++
语言开发WebAssembly
应用的标准工具,是WebAssembly
宿主接口事实上的标准之一。
Emscripten
包含了将C/C++
代码编译为WebAssembly
所需的「完整工具集」(LLVM/Node.js/Python/Java
等),不依赖于任何其他的编译器环境。
可以使用emsdk
命令行工具安装Emscripten
。
emsdk
是一组基于Python
的脚本。我们可以在Python 官网下载并安装最新版的Python
。
$ python --version // 3.11.2
Python
准备就绪后,下载emsdk
工具包。
// 下载emsdk
$ git clone https://github.com/emscripten-core/emsdk.git
在控制台切换到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
。可以使用如下步骤:
❝
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
程序。
「由于我们是用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
后是无法直接运行的。我们需要将其导入网页,通过浏览器来执行。
我们在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
的输出结果。
WebAssembly
程序也可以在Node.js 8+
版本中运行。
如果大家对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
,并为你加载模块。
使用 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
汇编模块(即.wasm
)的载入是在instantiateAsync
中完成的。
上述代码就做了几件事
WebAssembly.instantiateStreaming()
创建wasm
模块的实例WebAssembly.instantiate()
方法创建实例receiveInstance
方法处理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
。
「分享是一种态度」。