过去属于死神,未来属于你自己。------雪莱
大家好,我是柒八九。
前言
最近,不是加大了对Rust
相关文章的输出吗,在评论区或者私信区。有一些不同的声音说:"Rust没有前途,然后...."。其实呢,看一个技术是否有需要学习的动力。想必大家的底层理由都是一切都是向钱看,毕竟在国内大家都是业务为主,想自己纯手搞一套符合自己的技术框架和范式,这是不切实际的。(当然也不能一杆子打死,还是有很多技术大牛的)现在大家纠结或者对这个技术属于观望态度,无非就是在平时开发工作中没有涉及到的点。
同时,由于国内技术的滞后性,有一些应用场景其实还是处于蛮荒的状态。(不是崇洋媚外,事实确实如此)。所以,在一些可以用到新的技术点的方向上,国内还是处于蓝海阶段。
所以,本着对该技术的独有关注度,我还是选择义无反顾的投身到学习和实际中。冲破黎明之前的黑暗,你会拥有太阳,而晨曦中第一缕阳光也是为你而耀眼 。
而具体,Rust
到底能给你带来点啥,我们之前有文章讲过,这里就不在赘述了。
Last but not leaset
,由于现在本人暂时专注于前端领域居多,所以我更多关注Rust
能为前端带来点啥。而说到Rust
和前端,第一点的联想就是:WebAssembly
。(如果,不了解何为WebAssembly
,可以参考我们之前的文章浏览器第四种语言-WebAssembly,里面的例子是用Emscripten
写的)
其实,我们之前写过如何用C
写wasm
,也写过WebAssembly-C与JS互相操作等文章。但是,由于一些不可言喻的原因搁置了。
我们今天将使用Rust
创建一个WebAssembly Hello World
程序。我们将深入了解由wasm-bindgen
生成的代码,以及它们如何共同协作来帮助我们进行开发。我们还将使用wabt
来探索生成的wasm
代码。这将使我们更好地理解Rust WebAssembly
,并为我们的开发奠定良好的基础。
好了,天不早了,干点正事哇。
我们能所学到的知识点
- 前置知识点
- 项目搭建
- 原理探析
- 内容拓展
1. 前置知识点
前置知识点 ,只是做一个概念的介绍,不会做深度解释。因为,这些概念在下面文章中会有出现,为了让行文更加的顺畅,所以将本该在文内的概念解释放到前面来。如果大家对这些概念熟悉,可以直接忽略
同时,由于阅读我文章的群体有很多,所以有些知识点可能我视之若珍宝,尔视只如草芥,弃之如敝履 。以下知识点,请酌情使用。
安装Rust
如果是你一个Rust
萌新,我们也给你提供Rust环境配置和入门指南。
如果,想独立完成安装,可以到Rust 安装页面跟着教程安装。
在安装成功Rust
后,它会安装一个名为rustup
的工具,这个工具能让我们管理多个不同版本的 Rust
。默认情况下,它会安装用于惯常 Rust
开发的 stable
版本 Rust Release
。
Rustup
会安装
Rust
的编译器rustc
Rust
的包管理工具cargo
Rust
的标准库rust-std
- 以及一些有用的文档
rust-docs
因为,我本机已经安装好了Rust
。我们可以通过rustup --version
来查看rustup
的版本。以下是我本机的rustup
版本信息。下文中所有的代码,都基于该版本。
bash
rustup --version
rustup 1.26.0 (5af9b9484 2023-04-05)
安装WebAssembly二进制工具包(wabt)
这些工具旨在用于开发工具链或其他系统,这些系统希望操作WebAssembly文件 。与WebAssembly
规范解释器不同(该解释器旨在尽可能简单、声明性和"规范性"),这些工具是用C/C++
编写的,并设计成更容易集成到其他系统中。这些工具不旨在提供优化平台或更高级的编译目标;相反,它们旨在实现与规范的完全适应和遵从。
我们可以利用brew
来在Mac
环境下安装。
2. 项目搭建
2.1 安装wasm-bindgen
我们可以通过cargo install --list
来查看在$HOME/.cargo/bin
位置安装过的Rust
二进制文件。
在一些其他的教程中可以不使用wasm-bindgen
构建Hello World
程序,但是在本文中,我们将使用它,因为它在Rust WebAssembly
开发中是必不可少的。
bash
cargo install wasm-bindgen-cli
Rust WebAssembly
允许我们将WebAssembly模块
有针对性地插入到现有的JavaScript
应用程序中,尤其是在性能关键的代码路径 中。我们可以将wasm-bindgen
视为一种工具,它通过生成用于JavaScript
和WebAssembly
之间高效交互的粘合代码和绑定来帮助我们实现丝滑的交互体验。
2.2 创建Rust WebAssembly项目
巴拉拉小魔仙,念诵如下咒语,构建一个Rust WebAssembly
项目。
sql
cargo new hello_world --lib
上面的代码是使用Cargo
工具创建一个新的Rust
项目,项目的名称是hello_world
,并且指定它是一个库(--lib
)。这将创建一个包含基本项目结构的文件夹,其中包括一个Cargo.toml
文件和一个src
文件夹。
shell
+-- Cargo.toml
+-- src
+-- lib.rs
Cargo.toml
文件用于管理项目的依赖和配置src
文件夹包含项目的Rust源代码文件
- 项目名称
hello_world
是一个示例名称,我们可以根据自己的需求为项目指定不同的名称。
2.3 修改Cargo.toml
配置项
使用宇宙最强IDE -VScode
,打开Cargo.toml
文件。我们应该会看到以下内容。
toml
[package]
name = "hello_world"
version = "0.1.0"
authors = ["789"]
edition = "2021"
[dependencies]
将其修改成下面的内容
toml
[package]
name = "hello_world"
version = "0.1.0"
authors = ["789"]
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
上面的大部分字段大家都能看懂,其中lib
项的配置,这里稍微解释一下:
crate-type = ["cdylib"]
: 这一行指定了生成的库的类型 。在这里,crate-type
设置为["cdylib"]
,这表示我们正在创建一个动态链接库(C-compatible dynamic library
)。这用于编译一个供其他编程语言加载的动态库。此输出类型将在Linux
上创建*.so
文件,在macOS
上创建*.dylib
文件,在Windows
上创建*.dll
文件。
这种类型的库可以被其他编程语言调用,因为它们与C语言兼容。这对于与WebAssembly(Wasm)互操作性很重要,因为Wasm
通常需要与C语言
接口进行交互。因此,cdylib
表示该库是一个可供其他语言使用的动态链接库
。
2.4 编辑lib.rs
打开src/lib.rs
文件。将其更改为以下内容:
rust
extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;
// 导入 'window.alert'
#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
}
// 导出一个 'helloworld' 函数
#[wasm_bindgen]
pub fn helloworld(name: &str) {
alert(&format!("Hello World : {}!", name));
}
我们简单解释一下核心代码:
-
extern crate wasm_bindgen;
: 这一行声明了对wasm_bindgen
库的依赖。wasm_bindgen
是一个Rust库,用于构建Wasm
模块并提供与JavaScript
的互操作性。在Rust
当中,库被称为crates
,因为我们使用的是一个外部库,所以有extern
。 -
use wasm_bindgen::prelude::*;
: 这一行导入了wasm_bindgen
库的预导出(prelude
)模块中的所有内容,以便在后续代码中使用。
在 Rust 中调用来自 JavaScript 的外部函数
rust
#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
}
#[wasm_bindgen]
: 在 #[]
中的内容叫做 "属性",并以某种方式改变下面的语句。#[wasm_bindgen]
是一个属性标记 ,用于指定与WebAssembly
互操作相关的特性。
extern "C" { fn alert(s: &str); }
: 这里声明了一个外部函数 alert
,它使用extern "C"
指定了C ABI
(应用二进制接口),这意味着它可以与C语言进行交互 。这个alert
函数没有在Rust
中实现,而是在JavaScript
中实现,用于在浏览器中显示警告框。
在 JavaScript 中调用的 Rust 函数
rust
#[wasm_bindgen]
pub fn helloworld(name: &str) {
alert(&format!("Hello World : {}!", name));
}
#[wasm_bindgen] pub fn helloworld(name: &str)
: 这是一个Rust
函数helloworld
,它被标记为wasm_bindgen
,这意味着它可以被JavaScript
调用 。这个函数接受一个字符串参数 name
,然后调用之前声明 的alert
函数,以显示带有Hello World
消息的弹框,并在消息中包括name
参数的内容。
2.5 编译代码
在命令行中输入以下命令:
bash
cargo build --target wasm32-unknown-unknown
如果未安装对应的库,控制台会给出提示。 那我们就照猫画虎的操作一下:
bashrustup target add wasm32-unknown-unknown
-
cargo build
: 这是Cargo
工具的命令,用于构建Rust
项目。它会编译项目的源代码并生成可执行文件或库文件,具体取决于项目的类型。 -
--target wasm32-unknown-unknown
: 这部分是构建的目标参数。--target
标志用于指定要构建的目标平台。在这里,wasm32-unknown-unknown
是指定了WebAssembly
目标平台。这告诉Cargo
生成适用于WebAssembly
的二进制文件,而不是生成本地平台的二进制文件。
当运行这个命令后,Cargo
会使用 Rust
编译器(Rustc
)以及与 WebAssembly
相关的工具链,将 Rust
代码编译为 WebAssembly
格式的二进制文件。这个生成的 Wasm
文件可以在浏览器中运行,或与其他支持 WebAssembly
的环境一起使用。
运行结果如下:
cargo build --target wasm32-unknown-unknown
命令的默认输出位置 是在项目的 target
目录下,具体位置是:
shell
target/wasm32-unknown-unknown/debug/
在这个目录下,我们会找到生成的 WebAssembly
文件(通常是一个 .wasm
文件),以及其他与编译过程相关的文件。
2.6 构建Web服务器
既然,我们通过上述的魔法,将Rust
程序编译为了可以在浏览器环境下引用执行的格式。为了这口醋,我们还专门包顿饺子 。
我们需要一个Web服务器
来测试我们的WebAssembly
程序。我们将使用Webpack
,我们需要创建三个文件:index.js
、package.json
和webpack.config.js
。
下面的代码,我们最熟悉不过了,就不解释了。
index.js
javascript
// 直接引入了,刚才编译后的文件
const rust = import('./pkg/hello_world.js');
rust
.then(m => m.helloworld('World!'))
.catch(console.error);
package.json
json
{
"scripts": {
"build": "webpack",
"serve": "webpack-dev-server"
},
"devDependencies": {
"@wasm-tool/wasm-pack-plugin": "0.4.2",
"text-encoding": "^0.7.0",
"html-webpack-plugin": "^3.2.0",
"webpack": "^4.29.4",
"webpack-cli": "^3.1.1",
"webpack-dev-server": "^3.1.0"
}
}
webpack.config.js
javascript
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
module.exports = {
entry: './index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index.js',
},
plugins: [
new HtmlWebpackPlugin(),
new WasmPackPlugin({
crateDirectory: path.resolve(__dirname, ".")
}),
// 让这个示例在不包含`TextEncoder`或`TextDecoder`的Edge浏览器中正常工作。
new webpack.ProvidePlugin({
TextDecoder: ['text-encoding', 'TextDecoder'],
TextEncoder: ['text-encoding', 'TextEncoder']
})
],
mode: 'development'
};
安装指定的依赖。
bash
npm install webpack --save-dev
npm install webpack-cli --save-dev
npm install webpack-dev-server --save-dev
npm install html-webpack-plugin --save-dev
npm install @wasm-tool/wasm-pack-plugin --save-dev
npm install text-encoding --save-dev
2.7 构建&运行程序
使用npm run build
构建程序。
使用npm run serve
运行Hello World
程序
在浏览器中打开localhost:8080
,我们将看到一个显示 Hello World!
的弹窗。
到目前为止,我们已经构建了一个wasm
并且能够和js
实现功能交互的项目。其实,到这里已经完成了,我们这篇文章的使命。但是,在这里戛然而止,感觉缺失点啥。所以,我们继续深挖上面的项目的实现原理。
3. 原理探析
在使用cargo
和wasm_bindgen
编译源代码时,会在pkg
文件中自动生成以下文件:
- "hello_world_bg.wasm"
- "hello_world.js"
- "hello_world.d.ts"
- "package.json"
这些文件也可以通过使用以下wasm-bindgen
命令手动生成
:
bash
wasm-bindgen target/wasm32-unknown-unknown/debug/hello_world.wasm --out-dir ./pkg
浏览器调用顺序
以下显示了当我们在浏览器中访问localhost:8080
时发生的函数调用序列。
- index.js
- hello_world.js (调用hello_world_bg.js)
- helloworld_bg.wasm
index.js
javascript
const rust = import('./pkg/hello_world.js');
rust
.then(m => m.helloworld('World!'))
.catch(console.error);
index.js
导入了 hello_world.js
并调用其中的 helloworld
函数。
hello_world.js
下面是hello_world.js
的内容,在其中它调用了helloworld_bg.wasm
javascript
import * as wasm from "./hello_world_bg.wasm";
import { __wbg_set_wasm } from "./hello_world_bg.js";
__wbg_set_wasm(wasm);
export * from "./hello_world_bg.js";
hello_world_bg.js
javascript
// ...省去了部分代码
export function helloworld(name) {
const ptr0 = passStringToWasm0(name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
wasm.helloworld(ptr0, len0);
}
hello_world_bg.js
文件是由wasm-bindgen
自动生成的,它包含了用于将DOM
和JavaScript
函数导入到Rust
中的JavaScript粘合代码
。它还在生成的WebAssembly
函数上向JavaScript
公开了API。
Rust WebAssembly
专注于将WebAssembly
与现有的JavaScript
应用程序集成在一起。为了实现这一目标,我们需要在JavaScript
和WebAssembly
函数之间传递不同的值、对象或结构。这并不容易,因为需要协调两个不同系统的不同对象类型。
更糟糕的是,当前
WebAssembly
仅支持整数 和浮点数 ,不支持字符串。这意味着我们不能简单地将字符串传递给WebAssembly
函数。
要将字符串
传递给WebAssembly
,我们需要将字符串转换为数字 (请注意在webpack.config.js
中指定的TextEncoderAPI
),将这些数字放入WebAssembly
的内存空间中,最后返回一个指向字符串的指针 给WebAssembly
函数,以便在JavaScript
中使用它。在最后,我们需要释放WebAssembly
使用的字符串内存空间。
如果我们查看上面的JavaScript
代码,这正是自动执行的操作。helloworld
函数首先调用passStringToWasm
。
- 这个函数在
WebAssembly
中创建一些内存空间 ,将我们的字符串转换为数字,将数字写入内存空间,并返回一个指向字符串的指针。 - 然后将指针传递给
wasm.helloworld
来执行JavaScript
的alert
。最后,wasm.__wbindgen_free
释放了内存。
如果只是传递一个简单的字符串,我们可能可以自己处理,但考虑到当涉及到更复杂的对象和结构时,这个工作会很快变得非常复杂。这说明了wasm-bindgen
在Rust WebAssembly
开发中的重要性。
反编译wasm到txt
在前面的步骤中,我们注意到wasm-bindgen
生成了一个hello_world.js
文件,其中的函数调用到我们生成的hello_world_bg.wasm
中的WebAssembly
代码。
基本上,
hello_world.js
充当其他JavaScript
(如index.js
)与生成的WebAssembly
的helloworld_bg.wasm
之间的桥梁。
我们可以通过输入以下命令进一步探索helloworld_bg.wasm
:
bash
wasm2wat hello_world_bg.wasm > hello_world.txt
这个命令使用wabt
将WebAssembly
转换为WebAssembly文本格式
,并将其保存到一个hello_world.txt
文件中。打开helloworld.txt
文件,然后查找$helloworld
函数。这是我们在src/lib.rs
中定义的helloworld
函数的生成WebAssembly
函数。
$helloworld函数
在helloworld.txt
中查找以下行:
plaintext
(export "helloworld" (func $helloworld))
这一行导出了wasm.helloworld
供宿主调用的WebAssembly
函数。我们通过hello_world_bg.js
中的wasm.helloworld
来调用这个WebAssembly
函数。
接下来,查找以下行:
plaintext
(import "./hello_world_bg.js" "__wbg_alert_9ea5a791b0d4c7a3" (func $hello_world::alert::__wbg_alert_9ea5a791b0d4c7a3::h93c656ecd0e94e40 (type 4)))
这对应于在hello_world_bg.js
中生成的以下JavaScript
函数:
javascript
export function __wbg_alert_9ea5a791b0d4c7a3() { return logError(function (arg0, arg1) {
alert(getStringFromWasm0(arg0, arg1));
}, arguments) };
这是wasm-bindgen
提供的粘合部分 ,帮助我们在WebAssembly
中使用JavaScript
函数或DOM
。
最后,让我们看看wasm-bindgen
生成的其他文件。
hello_world.d.ts
这个.d.ts
文件包含JavaScript
粘合的TypeScript
类型声明,如果我们的现有JavaScript
应用程序正在使用TypeScript
,它会很有用。我们可以对调用WebAssembly
函数进行类型检查 ,或者让我们的IDE提供自动完成。如果我们不使用TypeScript
,可以安全地忽略这个文件。
package.json
package.json
文件包含有关生成的JavaScript
和WebAssembly
包的元数据。它会自动从我们的Rust
代码中填充所有npm依赖项,并使我们能够发布到npm
。
4. 内容拓展
再次看一下以下代码:
hello_world_bg.js
javascript
function helloworld(name) {
const ptr0 = passStringToWasm0(name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
wasm.helloworld(ptr0, len0);
}
该代码用于分配和释放内存,这一切都是由程序自动处理 的。不需要垃圾回收器或完整的框架引擎,使得使用Rust
编写的WebAssembly
应用程序或模块变得小巧且优化。其他需要垃圾回收器的语言将需要包含用于其底层框架引擎的wasm
代码。因此,无论它们有多么优化,其大小都不会小于Rust
提供的大小。这使得Rust WebAssembly
成为一个不错的选择,如果我们需要将小型WebAssembly
模块集成或注入到JavaScript Web
应用程序中。
除了Hello World
之外,还有一些其他需要注意的事项:
web-sys
使用wasm-bindgen
,我们可以通过使用extern
在Rust WebAssembly
中调用JavaScript
函数。请记住src/lib.rs
中的以下代码:
rust
#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
}
Web
具有大量API,从DOM
操作到WebGL
再到Web Audio
等等。因此,如果我们的Rust WebAssembly
程序增长,并且我们需要对Web API
进行多次不同的调用,我们将需要花时间编写大量的extern
代码。
web-sys
充当wasm-bindgen
的前端,为所有Web API提供原始绑定。
这意味着如果我们使用web-sys
,可以节省时间,而不必编写extern
代码。
引入web-sys
将web-sys
添加为Cargo.toml
的依赖项:
toml
[dependencies]
wasm-bindgen = "0.2"
[dependencies.web-sys]
version = "0.3"
features = [
]
为了保持构建速度非常快,web-sys
将每个Web接口都封装在一个Cargo
特性后面。在API文档中找到我们要使用的类型或方法;它将列出必须启用的特性才能访问该API。
例如,如果我们要查找window.resizeTo
函数,我们会在API文档中搜索resizeTo
。我们将找到web_sys::Window::resize_to
函数,它需要启用Window
特性。要访问该函数,我们在Cargo.toml
中启用Window
特性:
toml
[dependencies.web-sys]
version = "0.3"
features = [
"Window"
]
调用这个方法:
rust
use wasm_bindgen::prelude::*;
use web_sys::Window;
#[wasm_bindgen]
pub fn make_the_window_small() {
// 调整窗口大小为500px x 500px。
let window = web_sys::window().unwrap();
window.resize_to(500, 500)
.expect("无法调整窗口大小");
}
这段代码的目的是调整浏览器窗口的大小为500x500
像素,并演示了如何使用web-sys
和启用的Cargo
特性来调用Web API。
后记
分享是一种态度。
全文完,既然看到这里了,如果觉得不错,随手点个赞和"在看"吧。