浏览器第四种语言-WebAssembly

简介: WebAssembly 是个啥? 推荐阅读指数⭐️⭐️⭐️⭐️⭐️使用 Emscripten 写一个属于你的 wasm 推荐阅读指数⭐️⭐️⭐️⭐️⭐️胶水代码 推荐阅读指数⭐️⭐️⭐️⭐️编译目标及编译流程 推荐阅读指数⭐️⭐️⭐️


先认识它,再驾驭它

大家好,我是柒八九

ChatGPT知道吧!现在最新的new Bing中已经接入了AI功能。

而能够实现上述让人欲罢不能的功能。OenAI是永远绕不开的话题。

OpenAI 是一家人工智能研究机构,他们在 2020 年推出了一款基于 WebAssembly 的 AI 模型推理引擎,名为 MicroscopeMicroscope 可以在现代浏览器中运行,提供了高效的 AI 模型推理能力。

既然,AI的模型,我们搞不定;那么WebAssembly这种更贴近前端开发者的技术,我们还是可以窥探一番的。

好了,天不早了,我们开始今天WebAssembly基础知识的探索之旅。

你能所学到的知识点

  1. WebAssembly 是个啥?  推荐阅读指数⭐️⭐️⭐️⭐️⭐️
  2. 使用 Emscripten 写一个属于你的 wasm  推荐阅读指数⭐️⭐️⭐️⭐️⭐️
  3. 胶水代码  推荐阅读指数⭐️⭐️⭐️⭐️
  4. 编译目标及编译流程  推荐阅读指数⭐️⭐️⭐️

WebAssembly是个啥?

WebAssembly(简称Wasm)是一种可以在现代Web浏览器中运行的低级字节码

  • 它是一种可移植、大小合理和加载速度快的格式,适用于Web上的各种应用程序。

WebAssembly 是一种新的编程语言,并不是JavaScript的替代品。相反,它是一种补充,可以与现有的Web技术一起使用。WebAssembly 可以被编译成 JavaScript,也可以直接在浏览器中运行。

WebAssembly 也是新一代Web 虚拟机标准,可以让用各种语言编写的代码都能以接近原生的速度在Web中运行

  1. C/C++代码可以通过Emscripten工具链编译为wasm二进制文件,进而导入网页中供js调用
  2. 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的。

  1. V8接收到要执行的JS 源代码
  • 源代码V8 来说只是一堆字符串V8 并不能直接理解这段字符串的含义
  1. V8结构化这段字符串,生成了{抽象语法树|AST},同时还会生成相关的作用域
  2. 生成字节码(介于AST机器代码的中间代码)
  • 与特定类型的机器代码无关
  1. 解释器(ignition),按照顺序解释执行字节码,并输出执行结果。

通过V8js转换为字节码然后经过解释器执行输出结果的方式执行JS,有一个弊端就是,如果在浏览器中再次打开相同的页面,当页面中的 JavaScript 文件没有被修改,再次编译之后的二进制代码也会保持不变,意味着编译这一步浪费了 CPU 资源

为了,更好的利用CPU资源,V8采用JIT(Just In Time)技术提升效率:而是混合编译执行和解释执行这两种手段

JIT引入了两个编译器

  1. 基线编译器
  • 如果一段代码变成了 warm,那么 JIT 就把它送到编译器去编译,并且把编译结果存储起来
  1. 优化编译器
  • 如果一个代码段变得 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 TechnologiesUnity 是一家游戏引擎和游戏开发工具提供商,他们在 2018 年推出了一款基于 WebAssembly 的游戏引擎,名为 "Unity 2018.2"。这款引擎可以在现代浏览器中运行,提供了与原生应用程序相同的性能和功能。
  • FastlyFastly 是一家内容传递网络(CDN)提供商,他们在 2019 年推出了一款名为 "Lucet" 的 WebAssembly 运行时。Lucet 可以在云端和边缘设备上运行 WebAssembly 代码,提供了比传统服务器更高的性能和可扩展性。
  • FigmaFigma 是一款基于 Web 的界面设计工具,他们在 2020 年推出了一款名为 "FigJam" 的新产品,其中使用了 WebAssembly 技术。FigJam 可以在浏览器中实时协作,并提供了高效的图形处理能力。
  • OpenAIOpenAI 是一家人工智能研究机构,他们在 2020 年推出了一款基于 WebAssembly 的 AI 模型推理引擎,名为 MicroscopeMicroscope 可以在现代浏览器中运行,提供了高效的 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。可以使用如下步骤:

  1. vim ~/.bash_profile
  2. 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.ccC源文件。

#include <stdio.h>
int main(){
  printf("hello,world!\n");
  return 0;
}
复制代码

进入控制台,执行以下命令进行编译:

emcc hello.cc
复制代码

hello.cc所在的目录下得到两个文件

  1. a.out.wasm
  • 该文件为C源文件编译后形成的WebAssembly汇编文件
  1. a.out.js
  • Emscripten生成的胶水代码,其中包含了Emscripten的运行环境和.wasm文件的封装
  • 导入a.out.js既可自动完成.wasm文件的载入/编译/实例化、运行时初始化等工作。

我们还可以使用-o选项指定emcc的输出文件

emcc hello.cc -o hell.js
复制代码

hello.cc所在的目录下得到两个文件 分别为 hello.wasmhello.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,需要遵循以下步骤:

  1. 编写 WebAssembly 模块,可以使用 C/C++、Rust 等语言编写。
  2. WebAssembly 模块编译为 wasm 格式。
  3. JavaScript 中加载 wasm 模块。
  4. 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
  });
复制代码

在上面的例子中,我们

  1. 首先使用 fetch 函数加载 wasm 模块,
  2. 然后使用 WebAssembly.instantiate 函数将其实例化。
  3. 最后,我们可以通过 results.instance.exports 对象访问 wasm 模块中的函数,并在 JavaScript 中调用它们。

胶水代码

Emscripten在编译时,生成了大量的JS胶水代码。

我们通过VScode打开hello.js发现,大多数的操作都围绕全局对象Module展开。该对象正是Emscripten程序运行的核心所在。

我们可以通过vscode快捷键Ctrl+K+0将所有函数折叠起来。这样方便查看顶层函数的定义。

WebAssembly汇编模块载入

WebAssembly汇编模块(即.wasm)的载入是在instantiateAsync中完成的。

上述代码就做了几件事

  1. 尝试使用 WebAssembly.instantiateStreaming()创建wasm模块的实例
  2. 如果流式创建失败,改用WebAssembly.instantiate()方法创建实例
  3. 成功实例化后返回值交由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可以设定两种不同的编译目标

  1. WebAssembly
  2. 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.jswasm


后记

分享是一种态度

参考地址

  1. emscripten.org
  2. WebAssembly
  3. 面向WebAssembly编程

全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。


相关文章
|
11月前
|
机器学习/深度学习 网络协议 数据建模
世界各国浏览器语言代码本地化对照表
世界各国浏览器语言代码本地化对照表
|
XML JavaScript 前端开发
可在JVM、nodejs和浏览器运行的静态编译语言 Ceylon 发布 1.0 版
Ceylon是一个可运行于JVM、nodejs和浏览器的静态OO语言。
133 0
可在JVM、nodejs和浏览器运行的静态编译语言 Ceylon 发布 1.0 版
|
Web App开发 PHP iOS开发
使用php判断浏览器的类型和语言的函数代码
用PHP判断浏览器类型其实很简单。因为浏览器在和服务器连接时候都会先发送一些包含自己信息的内容(浏览器类型、语言等) 我们经常看到有一些网站上面会显示出你目前使用的浏览器类型和使用的语言,比如显示的是:您使用的浏览器为 IE6,繁体字。看起来是不是很炫。 其实这样的功能不难实现,无非就是判断浏览器的类型和语言,如果用JS来做应该很简单,这里我们看看用PHP来怎么实现这样的功能,既然
1399 0
|
7月前
|
Web App开发 前端开发 JavaScript
|
7天前
解决win10无法打开自带的IE浏览器的问题
解决win10无法打开自带的IE浏览器的问题
21 0
|
2月前
|
存储 机器人
在阿里云RPA中,你可以通过以下步骤来更改默认唤醒IE浏览器的地址
【2月更文挑战第28天】在阿里云RPA中,你可以通过以下步骤来更改默认唤醒IE浏览器的地址
21 1
|
8月前
|
Web App开发 XML 编解码
IE浏览器下载文件中文文件名乱码问题解决
IE浏览器下载文件中文文件名乱码问题解决
87 0

热门文章

最新文章