发布于 2026-01-06 0 阅读
0

用 Rust 编写 WASM 模块

用 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
Enter fullscreen mode Exit fullscreen mode

请注意,要试用我们的模块,您还需要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
Enter fullscreen mode Exit fullscreen mode

我们还需要向Cargo.toml文件中添加动态库标志。通常情况下,它会告诉 Cargo 我们想要创建一个动态系统库,但当与 WebAssembly 目标一起使用时,它仅仅意味着“创建一个*.wasm不包含任何start函数的文件”。为此,我们可以添加以下代码片段:

[lib]
crate-type = ["cdylib"]
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

请注意, `extern C` 中的函数直接取自 JavaScript,这使得我们可以在 Rust 函数中调用它。如果我们编译这段代码并在 JavaScript 文件中执行,其效果与从普通 JavaScript 中alert调用它完全相同。alert()

我们可以将同样的逻辑应用于其他类型和函数——例如Vec<u8>JavaScript 中的缓冲区。缓冲区可以用以下两种方式之一表示:

  • 类型Uint8Array(直接等价于 JavaScript 中的Vec<u8>
  • ABuffer

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;
}
Enter fullscreen mode Exit fullscreen mode

现在,当我们编写 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)
}
Enter fullscreen mode Exit fullscreen mode

通过 wasm-bindgen-cli 构建

这里,我们需要将 Rust 编译成 WASM,方法是为wasm32-unknown-unknown目标构建我们的软件包,我们可以这样做:

cargo build --target=wasm32-unknown-unknown
Enter fullscreen mode Exit fullscreen mode

接下来,我们需要wasm-bindgen生成 JS 粘合代码,以确保所有功能正常运行。我们将使用一个nodejs目标,它会生成一个 CommonJS 模块并将其放在./pkg指定文件夹中,然后我们可以将其植入到任何需要的地方。

wasm-bindgen --target nodejs --out-dir ./pkg \
./target/wasm32-unknown-unknown/release/wasm_example.wasm
Enter fullscreen mode Exit fullscreen mode

现在我们可以将 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
Enter fullscreen mode Exit fullscreen mode

接下来,我们将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}`)
})
Enter fullscreen mode Exit fullscreen mode

这段代码片段执行以下操作:

  • 我们在 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
Enter fullscreen mode Exit fullscreen mode

完成后,您就可以使用它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())
}
Enter fullscreen mode Exit fullscreen mode

这样做的好处显而易见:

  • 我们不需要手动导入任何内容extern C
  • 我们可以轻松使用Node.js内部功能,而不会遇到任何问题。

当然,尽管 Rust 有很多优点,napi-rs但它仅兼容 Node.js。如果您想为浏览器编写一些 WASM 代码,则需要使用 Swiftwasm-packwasm-bindgenNode.js。此外,您还需要使用 Node 生态系统来保持 CLI 的更新,从 Rust 优先的角度来看,这有点奇怪。毋庸置疑,napi-rsRust 是使用 Rust 编写 Node.js 代码的非常便捷的方式。

即将结束

感谢阅读!Rust 与 WASM 具有极佳的互操作性,我们完全可以利用这一点来帮助我们在使用其他语言时。

阅读更多:

文章来源:https://dev.to/shuttle_dev/writing-a-wasm-module-in-rust-3jcf