用 Rust 编写 WASM 模块
大家好!今天的文章我们将讨论如何在 Rust 中编写 WebAssembly 模块。WebAssembly 是一种可移植的编译目标,它使编程语言能够方便地与 Web 上的 JavaScript 进行互操作。Rust 能够利用 WebAssembly 的优势,使其在许多应用场景中都非常有用,例如:
- CPU密集型工作负载(加密)
- GPU密集型工作负载(图像/视频处理、图像识别)
本文将重点介绍如何编写一个用于图像处理的 WASM 模块,该模块可在后端使用,并探讨部署 WASM 及其目标的常见方法。
入门
首先,你需要安装 Rust。如果没有安装,可以点击这里安装。
我们将重点尝试用三种不同的方式编写 WASM 模块:
- 使用
wasm-bindgenCLI - 使用
wasm-pack - 使用
napi-rs
我们将首先使用wasm-bindgen-cliRust 创建应用程序,然后再探讨如何使用它wasm-pack。本文的重点是创建一个简单的图像处理模块。Rust 可以显著提升应用程序在字节数组操作和数据处理方面的性能。
开始之前,请确保已wasm32-unknown-unknown安装目标程序。如果没有安装,可以按如下方式添加:
rustup target add wasm32-unknown-unknown
请注意,要试用我们的模块,您还需要npm安装(或任何替代方案)。
编写 WASM 模块
基础知识
为了搭建我们的项目,我们将首先使用cargo init --lib wasm-example创建一个新的库项目wasm-example。然后,我们将使用一小段 shell 命令安装依赖项:
cargo add wasm-bindgen@0.2.91
cargo add js-sys@0.3.68
cargo add image@0.24.9
我们还需要向Cargo.toml文件中添加动态库标志。通常情况下,它会告诉 Cargo 我们想要创建一个动态系统库,但当与 WebAssembly 目标一起使用时,它仅仅意味着“创建一个*.wasm不包含任何start函数的文件”。为此,我们可以添加以下代码片段:
[lib]
crate-type = ["cdylib"]
Rust 中的 JavaScript 类型
要在 Rust 中使用 JavaScript 类型,extern C除了使用wasm-bindgen宏之外,我们还需要使用其他方法。这样我们就可以直接从 JavaScript 导入函数到 Rust 中!
WASM 中的 Hello World 应用程序如下所示(摘自书中):
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet(name: &str) {
alert(&format!("Hello, {}!", name));
}
请注意, `extern C` 中的函数直接取自 JavaScript,这使得我们可以在 Rust 函数中调用它。如果我们编译这段代码并在 JavaScript 文件中执行,其效果与从普通 JavaScript 中alert调用它完全相同。alert()
我们可以将同样的逻辑应用于其他类型和函数——例如Vec<u8>JavaScript 中的缓冲区。缓冲区可以用以下两种方式之一表示:
- 类型
Uint8Array(直接等价于 JavaScript 中的Vec<u8>) - A
Buffer型
Buffer是 `Uint8Array` 的子类Uint8Array。这是因为 Node.js 最初发布时,还没有 `Uint8Array` 类型——这正是 `Uint8Array` 类型诞生的原因Buffer。后来,随着 ES6 引入 `Uint8Array`,两者最终合并,因为这样做更有意义。许多 JavaScript 库仍然使用 `Uint8Array` Buffer。通过使用`Uint8Array` js-sys,我们可以实现 JavaScript 和 Rust 之间的互操作性——我们可以在下面通过定义 `Uint8Array`Buffer类型并提供一个带有 ` buffer()get` 方法的 `get` 方法来看到这一点:
use js_sys::ArrayBuffer;
// This defines the Node.js Buffer type
#[wasm_bindgen]
extern "C" {
pub type Buffer;
#[wasm_bindgen(method, getter)]
fn buffer(this: &Buffer) -> ArrayBuffer;
#[wasm_bindgen(method, getter, js_name = byteOffset)]
fn byte_offset(this: &Buffer) -> u32;
#[wasm_bindgen(method, getter)]
fn length(this: &Buffer) -> u32;
}
现在,当我们编写 WASM 函数时,我们可以Buffer直接引用类型了!
让我们编写一个 Rust 函数来转换图像文件格式。我们将要求它引入我们的库Buffer,然后让它返回一个文件Vec<u8>——当我们通过 .jswasm-pack或其他编译器编译它时,它将自动转换为 .js 文件Uint8Array。
use js_sys::{ArrayBuffer, Uint8Array};
use wasm_bindgen::prelude::wasm_bindgen;
use image::ImageFormat;
use image::io::Reader;
use std::io::Cursor;
// .. extern C stuff goes here
#[wasm_bindgen]
pub fn convert_image(buffer: &Buffer) -> Vec<u8> {
// This converts from a Node.js Buffer into a Vec<u8>
let bytes: Vec<u8> = Uint8Array::new_with_byte_offset_and_length(
&buffer.buffer(),
buffer.byte_offset(),
buffer.length()
).to_vec();
let img2 = Reader::new(Cursor::new(bytes)).with_guessed_format().unwrap().decode().unwrap();
let mut new_vec: Vec<u8> = Vec::new();
img2.write_to(&mut Cursor::new(&mut new_vec), ImageFormat::Jpeg).unwrap();
Ok(new_vec)
}
通过 wasm-bindgen-cli 构建
这里,我们需要将 Rust 编译成 WASM,方法是为wasm32-unknown-unknown目标构建我们的软件包,我们可以这样做:
cargo build --target=wasm32-unknown-unknown
接下来,我们需要wasm-bindgen生成 JS 粘合代码,以确保所有功能正常运行。我们将使用一个nodejs目标,它会生成一个 CommonJS 模块并将其放在./pkg指定文件夹中,然后我们可以将其植入到任何需要的地方。
wasm-bindgen --target nodejs --out-dir ./pkg \
./target/wasm32-unknown-unknown/release/wasm_example.wasm
现在我们可以将 WASM 代码作为软件包发布,或者将其植入到任何我们想要使用它的地方!
我不想使用 CommonJS!
如果您因为使用 ESM(EcmaScript 模块或 ES6 模块)而不想使用 CommonJS,那也没问题!CLI 目前支持以下几个目标:
bundler(生成可用于 Webpack 等打包工具的代码)web(可直接在网页浏览器中加载)nodejs(可通过requireCommonJS Node.js 模块加载)deno(可用作 Deno 模块)no-modules(类似于web目标,但不使用 ES 模块)。
这里提供了专门用于 ES 的文档,您可以点击此处查看。就编译器而言,最简单的方法通常是使用 Webpack,因为它兼容性最好。此外,这里还有一份无需打包工具即可编译 ES6 模块的指南——不过它需要在运行前手动初始化 WASM 模块,这会增加一些开销。
试用我们的新模块
现在我们已经编写好了代码,让我们来试试吧!我们将使用 Express.js 启动一个 JavaScript 后端服务器。为了方便起见,我们假设您在 Rust 项目所在的文件夹中运行以下命令。我们将从以下 shell 代码片段开始:
npm init -y
npm i express express-fileupload
接下来,我们将server.js在根目录下创建一个文件,并插入以下代码:
const fileUpload = require('express-fileupload');
const express = require('express');
const { convert_image } = require('./pkg/wasmmeme.js');
const app = express();
const port = 3030;
app.get('/', (req, res) => {
res.send(`
<h2>With <code>"express"</code> npm package</h2>
<form action="/api/upload" enctype="multipart/form-data" method="post">
<div>Text field title: <input type="text" name="title" /></div>
<div>File: <input type="file" name="file"/></div>
<input type="submit" value="Upload" />
</form>
`);
});
app.post('/api/upload', (req, res, next) => {
const image = convertImage(req.files.file.data)
res.setHeader('Content-disposition', 'attachment; filename="meme.jpeg"');
res.setHeader('Content-type', 'image/jpg');
res.send(image);
});
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
这段代码片段执行以下操作:
- 我们在 3030 端口上搭建了一个 Express 服务器。
- 我们有一个路由,
/当我们在浏览器中访问它时,它会返回一个 HTML 表单。 - 我们有一个 API 路由,可以从我们的文件上传中获取数据,将其转换为新格式,设置正确的标头并返回新图像。
如果我们使用node server.js,请在浏览器中访问[http://localhost:3030](http://localhost:3030),然后填写表单并附加图像,我们应该会收到图像下载响应!
请注意,根据您用于图像文件格式转换的设置,转换后文件大小可能会增加;这是因为您可能使用了无损转换。如果您想使用有损转换来减小文件大小,则需要new_with_quality在 Rust 代码中实例化图像编码器时使用相应的方法。
使用替代命令行界面构建我们的应用程序
虽然wasm-bindgen-cli它很有用,但它也是我们所有选项中最底层的命令行界面,使用过程中可能会遇到wasm-bindgen一些莫名其妙的问题,例如版本不兼容。我们还可以改进一些其他功能,例如自动版本控制和wasm-opt使用情况管理,从而提升用户体验。接下来,我们快速浏览一下其他选项,看看它们之间的区别。
Wasm-pack
wasm-pack是一个旨在提供一站式 Rust 编译到 WASM 解决方案的工具。它包含一个命令行界面 (CLI),您可以通过在此处安装来使用它。与使用 相比wasm-bindgen-cli,它进行了多项提升用户体验的改进:
- 附带
wee_alloc一个 WebAssembly 分配器,其(压缩前)代码大小为 1kB。 - 它带有一个 panic hook,允许你在浏览器中调试 Rust 的 panic 信息。
为了初始化我们的项目,我们可以使用wasm-pack new wasm-example它,它会为我们完成所有操作。代码方面,我们的主函数(以及 C/JS 绑定)将保持不变,因为wasm-pack它主要提供工具增强功能以简化编译,并且没有任何可供我们使用的库代码。
napi-rs
napi-rs是一个用于用 Rust 构建预编译 Node.js 插件的框架。如果您觉得使用wasm-bindgen过于复杂,只想编写 Node.js 代码,那么这是一个不错的选择。使用它需要 Node v0.10.0 或更高版本。您可以使用以下 shell 命令片段进行安装(需要 npm 或其替代方案):
npm install -g @napi-rs/cli
完成后,您就可以使用它napi new wasm-example来构建您的新 NAPI 项目了!
napi-rs这确实带来了一些代码更改,您可以在下面看到:我们终于可以摆脱extern C代码块,转而使用 napibindgen_prelude来包含我们需要的任何内容。
use napi::bindgen_prelude::*;
use image::io::Reader;
use image::ImageFormat;
use image::ImageOutputFormat;
use std::io::Cursor
#[macro_use]
extern crate napi_derive;
#[napi]
pub fn convert_image(buffer: Buffer) -> Result<Buffer> {
let bytes: Vec<u8> = buffer.into();
let img2 = Reader::new(Cursor::new(bytes)).with_guessed_format().unwrap().decode().unwrap();
let mut new_vec: Vec<u8> = Vec::new();
img2.write_to(&mut Cursor::new(&mut new_vec), ImageFormat::Jpeg).unwrap();
Ok(new_vec.into())
}
这样做的好处显而易见:
- 我们不需要手动导入任何内容
extern C - 我们可以轻松使用Node.js内部功能,而不会遇到任何问题。
当然,尽管 Rust 有很多优点,napi-rs但它仅兼容 Node.js。如果您想为浏览器编写一些 WASM 代码,则需要使用 Swiftwasm-pack或wasm-bindgenNode.js。此外,您还需要使用 Node 生态系统来保持 CLI 的更新,从 Rust 优先的角度来看,这有点奇怪。毋庸置疑,napi-rsRust 是使用 Rust 编写 Node.js 代码的非常便捷的方式。
即将结束
感谢阅读!Rust 与 WASM 具有极佳的互操作性,我们完全可以利用这一点来帮助我们在使用其他语言时。
阅读更多:
- 我们整理了一份提升 Rust 开发效率的 8 大工具清单。
- 以下是我们关于如何开始使用 Axum(Rust 最流行的框架)的指南。
- 我们撰写了一篇关于8 款顶级工具的文章,旨在帮助您提高 Rust 开发效率。