在 Rust 中編寫 WASM 的三種方式

在這篇文章中,我們將討論如何在 Rust 中編寫 WebAssembly 模塊。WebAssembly 是可移植編譯目標,能夠方便地在 web 上與 JavaScript 互操作。Rust 能夠利用這一點,使得它在許多用例中非常有用。例如:

本文將重點介紹使用 Rust 編寫圖像處理的 WASM 模塊,並探討部署 WASM 的常用方法。

我們將重點嘗試用三種不同的方式編寫 WASM 模塊:

我們將首先使用 wasm-bindgen-cli 來創建我們的應用程序,然後使用 wasm-pack。本文的重點是創建一個簡單的圖像處理模塊,字節數組操作和數據處理是 Rust 可以顯著提高應用程序速度的領域。

在開始之前,請確保安裝了 wasm32-unknown-unknown。如果沒有,使用以下命令安裝:

rustup target add wasm32-unknown-unknown

請注意,爲了測試我們的模塊,還需要額外安裝 npm(或任何替代方案)。

構建項目

首先使用如下命令創建一個名爲 wasm-example 的新項目:

cargo init --lib wasm-example

然後,執行以下命令來安裝依賴項:

cargo add wasm-bindgen@0.2.91
cargo add js-sys@0.3.68
cargo add image@0.25.0

我們還需要將動態庫標誌添加到 Cargo.toml 文件中。通常,它讓 Cargo 知道我們想要創建一個動態系統庫——但是當它與 WebAssembly 目標一起使用時,它的意思是 “創建一個沒有啓動函數的 *.wasm 文件”:

[lib]
crate-type = ["cdylib"]

爲了能夠在 Rust 中使用 JavaScript 類型,除了使用 wasm-bindgen 宏之外,我們還需要使用 extern C。這允許我們直接從 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));
}

注意,外部 C 代碼中的 alert 函數直接取自 JavaScript,因此我們可以在 Rust 函數中調用它。如果我們要編譯它並在 JavaScript 文件中執行,它將與從常規 JavaScript 文件中調用 alert() 相同。

我們可以應用相同的邏輯來處理其他類型和函數——緩衝區。JavaScript 中的 Vec 可以用以下兩種方式表示:

Buffer 是 Uint8Array 的子類。這是因爲當 Node.js 第一次發佈時,沒有 Uint8Array 類型 - 這就是導致 Buffer 類型創建的原因。後來,當 Uint8Arrays 被引入 ES6 時,兩者最終被合併。許多 JavaScript 庫仍然使用 Buffer,通過使用 js-sys,我們可以在 JavaScript 和 Rust 之間實現互操作性。

讓我們在 src/lib.rs 文件中一個定義 Buffer 類型和提供一個方法 buffer() 方法:

use js_sys::{wasm_bindgen, ArrayBuffer};

// 這定義了Node.js的Buffer類型
#[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 函數。這個函數需要上面定義的緩衝區,它返回 Vec。當我們通過 wasm-pack 或其他編譯器編譯它時,它將自動轉換爲 Uint8Array。

use image::io::Reader;
use image::ImageFormat;
use js_sys::{wasm_bindgen, ArrayBuffer, Uint8Array};
use std::io::Cursor;
use wasm_bindgen::prelude::wasm_bindgen;

// .. extern C stuff goes here

#[wasm_bindgen]
pub fn convert_image(buffer: &Buffer) -> Vec<u8> {
    // 這將從Node.js緩衝區轉換爲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();

    new_vec
}

通過 wasm-bindgen-cli 構建

首先,執行以下命令安裝 wasm-bindgen-cli:

cargo install wasm-bindgen-cli

然後,我們需要構建 wasm32-unknown-unknown target,將 Rust 代碼編譯到 WASM,我們可以這樣做:

cargo build --target=wasm32-unknown-unknown

接下來,需要使用 wasm-bindgen 命令來生成 JS 粘合代碼,以使其正常工作。我們使用 nodejs target,它將生成一個 CommonJS 模塊,並將其放在./pkg 文件夾中,然後就可以將其植入到任何我們想要的地方。

wasm-bindgen --target nodejs --out-dir ./pkg ./target/wasm32-unknown-unknown/debug/wasm_example.wasm

現在我們可以將 WASM 代碼作爲包發佈。

如果你不想使用 CommonJS,比如你正在使用 ESM (EcmaScript 模塊,或 ES6 模塊),CLI 目前支持生成以下幾種 target:

就使用哪種編譯器而言,最簡單的方法通常是使用 Webpack,因爲它是最兼容的。

現在讓我們來測試一下!我們將使用 Express.js 啓動一個 JavaScript 後端服務器。在 Rust 項目根目錄下運行以下代碼 (爲了方便起見):

npm init -y
npm i express express-fileupload

接下來,我們將在項目根目錄中創建一個 server.js 文件,並插入以下代碼:

const express = require('express');
const fileUpload = require('express-fileupload');
const cors = require('cors');
const bodyParser = require('body-parser');
const morgan = require('morgan');
const _ = require('lodash');
const { convert_image } = require('./pkg/wasm_example.js');

const app = express();
const port = 3030;

app.use(fileUpload({
    createParentPath: true
}));

app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.use(morgan('dev'));

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"  /></div>
      <div>File: <input type="file" /></div>
      <input type="submit" value="Upload" />
    </form>
  `);
});

app.post('/api/upload'(req, res, next) ={
    const image = convert_image(req.files.file.data);

    res.setHeader('Content-disposition''attachment; file');
    res.setHeader('Content-type''image/jpg');
    res.send(image);
});

app.listen(port, () ={
  console.log(`Example app listening on port ${port}`)
})

這段代碼做了以下工作:

使用以下命令啓動服務器:

node server.js

然後在瀏覽器中輸入 http://localhost:3030/,如圖:

請注意,根據用於圖像文件格式轉換的設置,轉換後的文件大小可能會增加。這是因爲使用的是無損轉換。如果你想使用有損轉換來減小文件大小,需要在 Rust 代碼中實例化圖像編碼器時使用 new_with_quality 方法。

使用其他 CLI 構建

雖然 wasm-bindgen-cli 很有用,但它也是我們選項中最底層的 CLI,在使用它時可能會遇到一些問題,比如 wasm-bindgen 版本不兼容問題。讓我們快速瀏覽一下其他 CLI,並比較它們之間的異同。

Wasm-pack

wasm-pack 是一個旨在將 Rust 編譯爲 WASM 的一站式工具。它包含一個 CLI,可以使用如下命令來安裝它:

cargo install wasm-pack

與使用 wasm-bindgen-cli 相比,它有許多質量上的升級:

要初始化我們的項目,可以使用以下命令:

wasm-pack new wasm-example-2

它將爲我們做所有的事情。在代碼方面,我們的主函數 (和 C/JS 綁定) 將保持與 wasm-pack 相同,它主要提供工具添加,使編譯更容易。

然後使用如下命令構建 WASM:

wasm-pack build
或
wasm-pack build --target nodejs

napi-rs

napi-rs 是一個框架,用於在 Rust 中構建預編譯的 Node.js 插件。如果你發現使用 wasm-bindgen 太複雜而無法使用,並且只想編寫 Node.js 的東西,那麼這是一個很好的選擇。要使用它。可以使用如下命令來安裝它 (需要 npm 或它的替代品):

npm install -g @napi-rs/cli

安裝完成後,就可以使用以下命令來構建新的 napi 項目了:

napi new wasm-example-3

napi-rs 確實帶來了一些代碼的變化,你可以在下面看到:我們最終可以擺脫 “extern C” 塊,而是使用 napi 的 bindgen_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())
}

這樣做的好處是顯而易見的:

當然,儘管有這些優點,但是 napi-rs 只與 Node.js 兼容,同時還需要使用 Node 生態系統來更新 CLI,從 rust 優先的角度來看,這是一個有點奇怪的方式。但是,napi-rs 是用 Rust 開始編寫 Node.js 的一種非常簡單的方法。如果要爲瀏覽器編寫一些通用 WASM 代碼,還是需要使用 wasm-pack 或 wasm-bindgen。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ULErveNYlnyHFdoH8-3peA