Rust 编写 WASM 入门

用 Rust 编写和编译 WASM

Hello world! 在今天的文章中,我们将讨论如何用 Rust 编写 WebAssembly 模块。 WebAssembly 是编程语言的可移植编译目标,能够方便地与 Web 上的 JavaScript 进行互操作。 Rust 能够利用这一点,使其对于许多场景都非常有用,例如:

  • CPU 密集型工作负载(加密)
  • GPU 密集型工作负载(图像/视频处理、图像识别)

本文将重点讨论编写可在后端使用的用于图像处理的 WASM 模块,以及探索编写和部署 WASM常见方法。

入门

首先,您需要安装 Rust。如果没有,您可以在here安装。

我们将重点尝试以三种不同的方式编写 WASM 模块:

  • 使用 wasm-bindgen CLI

  • 使用 wasm-pack

  • 使用 napi-rs

首先将使用 wasm-bindgen-cli 创建应用程序,然后查看使用 wasm-pack 。本文的重点是创建一个简单的图像处理模块。字节数组操作和数据处理是 Rust 可以显着加快应用程序速度的领域。

在开始之前,请确保已安装 wasm32-unknown-unknown target。如果没有,可以像这样添加:

bash 复制代码
rustup target add wasm32-unknown-unknown

注意,为了尝试我们的模块,您还需要安装 npm (或其他替代方案)。

编写 WASM 模块

基础

为了设置项目,使用 cargo init --lib wasm-example 创建一个名为 wasm-example 的library项目。然后将使用以下代码安装依赖项:

bash 复制代码
cargo add wasm-bindgen@0.2.91
cargo add js-sys@0.3.68
cargo add image@0.24.9

还想将动态库 flag 添加到 Cargo.toml 文件中。通常,它让 Cargo 知道我们想要创建一个动态系统库 - 但当它与 WebAssembly 目标一起使用时,它只是意味着"创建一个没有 start 函数的 *.wasm 文件" 。为此,可以在下面添加这个小片段:

toml 复制代码
[lib]
crate-type = ["cdylib"]

Rust 中的 JavaScript 类型

为了能够在 Rust 中使用 JavaScript 类型,除了使用 wasm-bindgen 宏之外,还需要使用 extern C 。这允许直接从 JavaScript 导入函数到 Rust!

WASM 中的 Hello World 应用程序如下所示:

rust 复制代码
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 中的 alert 函数直接来自 JavaScript,并且允许我们在 Rust 函数中调用它。如果我们要编译它并在 JavaScript 文件中执行它,则与从常规 JavaScript 调用 alert() 相同。

我们可以应用相同的逻辑来处理其他类型和函数 - 即buffers。 JavaScript 中的 Vec<u8> 可以用以下两种方式之一表示:

  • Uint8Array 类( Vec<u8> 的直接 JavaScript 等价类型)
  • Buffer 类型

BufferUint8Array 的子类。这是因为当 Node.js 首次发布时,还没有 Uint8Array 类型 - 这就是导致创建 Buffer 类型的原因。后来,当 ES6 引入 Uint8Arrays 时,两者最终被合并,因为这样做是有意义的。许多 JavaScript 库仍然使用 Buffer 。通过使用 js-sys ,我们可以获得 JavaScript 和 Rust 之间的互操作性 - 通过定义 Buffer 类型并提供带有 buffer() 方法的方法,我们可以在下面看到这一点:

rust 复制代码
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> - 通过 wasm-pack 或其他编译器编译它时,它会自动转换为 Uint8Array

rust 复制代码
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 构建

在这里,需要通过为 wasm32-unknown-unknown target 构建包来从 Rust 编译为 WASM,可以这样做:

bash 复制代码
cargo build --target=wasm32-unknown-unknown

接下来,需要使用 wasm-bindgen 生成 JS 粘合代码以使其正常工作。将使用 nodejs 目标,它将生成 CommonJS 模块并将其放入 ./pkg 文件夹中,然后可以将其植入到想要的任何位置。

bash 复制代码
wasm-bindgen --target nodejs --out-dir ./pkg \
./target/wasm32-unknown-unknown/release/wasm_example.wasm

现在可以将 WASM 代码作为包发布,或者将其植入到想要使用的任何地方!

不想使用 CommonJS!

如果您因为使用 ESM(EcmaScript 模块,或 ES6 模块)而不想使用 CommonJS,那也很easy! CLI 当前允许多个target:

  • bundler (生成与 Webpack 等捆绑器一起使用的代码)

  • web (可直接在网络浏览器中加载)

  • nodejs (可通过 require 作为 CommonJS Node.js 模块加载)

  • deno (可用作 Deno 模块)

  • no-modules (与 web 目标类似,但不使用 ES 模块)。

有一些与 ES 一起使用的特定文档,您可以在here.查看。就使用的编译器而言,最简单的方法通常是使用 Webpack,因为它是最兼容的。 here还有一个额外的指南,您可以使用它在没有捆绑器的情况下编译为 ES6 模块 - 尽管它涉及在运行之前手动初始化 WASM 模块,这会增加一些开销。

测试驱动新模块

现在已经编写了代码,尝试一下!将使用 Express.js 启动 JavaScript 后端服务器。假设您在 Rust 项目所在的同一文件夹中运行以下命令(为了方便起见)。将从以下 shell 片段开始:

bash 复制代码
npm init -y
npm i express express-fileupload

接下来,将在根目录中创建一个 server.js 文件并插入以下代码:

jsx 复制代码
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) 然后填写表单并上传图像,应该会收到图像下载响应!

注意,根据用于图像文件格式转换的设置,文件大小可能会在转换后增加;这是因为可能正在使用无损转换。如果想使用有损转换来减小文件大小,则需要在 Rust 代码中实例化图像编码器时使用 new_with_quality 方法。

使用替代 CLI 构建应用程序

虽然 wasm-bindgen-cli 很有用,但它也是我们选项中最低级别的 CLI,在使用它时可能会遇到问题,例如 wasm-bindgen 版本不兼容问题。我们可以从一些额外的包受益,例如自动版本控制和 wasm-opt 使用。让我们快速浏览一下其他一些选项,看看它们如何比较。

Wasm-pack

wasm-pack 是一个旨在将 Rust 编译为 WASM 的一站式工具。它包含一个 CLI,您可以通过在here安装它来使用它。与使用 wasm-bindgen-cli 相比,它提升了很多便捷性:

  • 附带 wee_alloc ,一个具有(预压缩)1kB 代码占用空间的 WebAssembly 分配器。

  • 带有一个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 或其替代品):

bash 复制代码
npm install -g @napi-rs/cli

完成后,您可以使用 napi new wasm-example 构建新的 NAPI 项目!

napi-rs 确实带来了一些代码更改,可以在下面看到:最终可以摆脱 extern C 块,转而使用 napi 的 bindgen_prelude 包括需要的一切。

rust 复制代码
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 内部结构,没有任何麻烦

当然,尽管有很多优点, napi-rs 只与 Node.js 兼容。如果想为浏览器编写一些 WASM 代码,则需要默认为 wasm-packwasm-bindgen 。此外,还需要使用 Node 生态系统来保持 CLI 更新,从 Rust 优先的角度来看,这是一个有点奇怪的决定。但是, napi-rs 任然是开始使用 Rust 编写 Node.js 的一种非常简单的方法。

总结

谢谢阅读! Rust 与 WASM 具有很好的互操作性,没有理由不利用这一点来帮助我们使用其他语言。


原文地址:Writing & Compiling WASM in Rust

相关推荐
想用offer打牌4 小时前
MCP (Model Context Protocol) 技术理解 - 第二篇
后端·aigc·mcp
崔庆才丨静觅4 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60615 小时前
完成前端时间处理的另一块版图
前端·github·web components
KYGALYX5 小时前
服务异步通信
开发语言·后端·微服务·ruby
掘了5 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅5 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅6 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
爬山算法6 小时前
Hibernate(90)如何在故障注入测试中使用Hibernate?
java·后端·hibernate
崔庆才丨静觅6 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment6 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端