前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >浏览器第四种语言-WebAssembly

浏览器第四种语言-WebAssembly

作者头像
前端柒八九
发布2023-03-23 19:54:25
1.3K0
发布2023-03-23 19:54:25
举报
文章被收录于专栏:柒八九技术收纳盒

❝先认识它,再驾驭它❞

大家好,我是「柒八九」

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 并不能直接理解这段字符串的含义
  2. V8结构化这段字符串,生成了抽象语法树AST,同时还会生成相关的「作用域」
  3. 生成字节码(介于 AST机器代码的中间代码)
    • 「与特定类型的机器代码无关」
  4. 「解释器」(ignition),「按照顺序解释执行字节码」,并输出执行结果。

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

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

「JIT」引入了两个编译器

  1. 「基线编译器」
    • 如果一段代码变成了 warm,那么 JIT 就把它送到「编译器」去编译,并且把编译结果存储起来。
  2. 「优化编译器」
    • 如果一个代码段变得 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 模型推理引擎,名为 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

代码语言:javascript
复制
$ python --version // 3.11.2

下载emsdk

Python准备就绪后,下载emsdk工具包。

代码语言:javascript
复制
// 下载emsdk
$ git clone https://github.com/emscripten-core/emsdk.git

安装并激活Emscripten

在控制台切换到emsdk所在目录。

针对MacOS或者Linux用户,可以按照下面的代码进行配置处理。

代码语言:javascript
复制
$ cd emsdk

运行以下emsdk命令从GitHub获取最新工具,并将其设置为「活动状态」

代码语言:javascript
复制
# 获取最新版本的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

代码语言:javascript
复制
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查看版本信息。

代码语言:javascript
复制
> 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源文件。

代码语言:javascript
复制
#include <stdio.h>

int main(){
  printf("hello,world!\n");
  return 0;
}

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

代码语言:javascript
复制
emcc hello.cc

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

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

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

代码语言:javascript
复制
emcc hello.cc -o hell.js

hello.cc所在的目录下得到两个文件 分别为 hello.wasmhello.js

代码引用

与原生代码不同,C/C++代码被编译为WebAssembly后是无法直接运行的。我们需要将其导入网页,通过浏览器来执行。

在HTML中引用JS

我们在vscode中使用emmet直接搞一个最简单的html。然后引入我们刚才生成的hello.js

代码语言:javascript
复制
<!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写了一个最简单的服务器。

代码语言:javascript
复制
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插件进行引入处理

代码语言:javascript
复制
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

代码语言:javascript
复制
import init from './example.wasm?init'

init().then((instance) => {
  instance.exports.test()
})

但是呢,如果你把上面利用Emscripten生成的hello.wasm会报错。

代码语言:javascript
复制
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

代码语言:javascript
复制
#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格式。以下是一个示例命令:

代码语言:javascript
复制
emcc hello.c -o hello.wasm -s WASM=1 -s EXPORTED_FUNCTIONS="['_main', '_add']"

该命令将_main_add函数作为可导出的函数,以便在WebAssembly模块中调用它们。然后,您可以将生成的WASM文件嵌入到HTML文件中,并使用JavaScript代码调用它们。

代码语言: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

receiveInstance中执行了下面的指令:

代码语言:javascript
复制
Module['asm'] = exports;

❝意思就是将wasm模块实例的「导出对象」传给Module的子对象asm。❞

异步加载

WebAssembly实例是通过WebAssembly.instantiateStreaming()WebAssembly.instantiate()方法创建的,而这两个方法均为「异步调用」,这就意味着.js加载完成时,Emscripten的运行时并未准备就绪。

就会出现在载入hello.js后,立即调用Module._main()会报错。

解决这一问题需要建立一种运行时准备就绪的通知机制。我们可以使用onRuntimeInitialized回调。

代码语言:javascript
复制
<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编程
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-03-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端柒八九 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 你能所学到的知识点
  • WebAssembly是个啥?
    • WebAssembly 诞生背景
      • Web的第四种语言
        • WebAssembly解决的痛点
          • WebAssembly 优点
            • WebAssembly应用
            • 使用 Emscripten 写一个属于你的 wasm
              • 安装 Emscripten
                • 下载最新版的Python
                • 下载emsdk
                • 安装并激活Emscripten
                • emcc 全局安装
                • 校验安装
              • 编码环节
                • 生成.wasm文件
                • 代码引用
            • 胶水代码
              • WebAssembly汇编模块载入
                • 异步加载
                • 编译目标及编译流程
                  • 编译目标的选择
                    • 编译流程
                    • 后记
                      • 参考地址
                      相关产品与服务
                      命令行工具
                      腾讯云命令行工具 TCCLI 是管理腾讯云资源的统一工具。使用腾讯云命令行工具,您可以快速调用腾讯云 API 来管理您的腾讯云资源。此外,您还可以基于腾讯云的命令行工具来做自动化和脚本处理,以更多样的方式进行组合和重用。
                      领券
                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档