在 Rust 中編寫 WASM 的三種方式
在這篇文章中,我們將討論如何在 Rust 中編寫 WebAssembly 模塊。WebAssembly 是可移植編譯目標,能夠方便地在 web 上與 JavaScript 互操作。Rust 能夠利用這一點,使得它在許多用例中非常有用。例如:
-
CPU 密集型工作 (加密)
-
GPU 密集型工作 (圖像 / 視頻處理、圖像識別)
本文將重點介紹使用 Rust 編寫圖像處理的 WASM 模塊,並探討部署 WASM 的常用方法。
我們將重點嘗試用三種不同的方式編寫 WASM 模塊:
-
使用 wasm-bindgen-cli
-
使用 wasm-pack
-
使用 napi-rs
我們將首先使用 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 可以用以下兩種方式表示:
-
Uint8Array 類型 (直接相當於 JavaScript 中的 Vec)
-
Buffer 類型
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:
-
bundler:生成用於 Webpack 等捆綁器的代碼
-
Web:可直接在 Web 瀏覽器中加載
-
nodejs:可通過 require 作爲 CommonJS 的 Node.js 模塊加載
-
deno:可用作 deno 模塊
-
no-modules:像 web 目標,但不使用 ES 模塊
就使用哪種編譯器而言,最簡單的方法通常是使用 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}`)
})
這段代碼做了以下工作:
-
在端口 3030 設置了一個 Express 服務器
-
我們在 / 上有一個路由,當我們在瀏覽器中訪問它時,它會給我們一個 HTML 表單
-
我們有一個 API 路由,它將從文件上傳中獲取數據,將其轉換爲新格式,設置正確的頭信息並返回新圖像。
使用以下命令啓動服務器:
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 相比,它有許多質量上的升級:
-
附帶一個 WebAssembly 分配器 wee_alloc,它的代碼佔用 (預壓縮) 爲 1kB。
-
附帶一個 panic 鉤子,可以在瀏覽器中調試 Rust panic 消息。
要初始化我們的項目,可以使用以下命令:
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())
}
這樣做的好處是顯而易見的:
-
我們不需要使用 “extern C” 手動導入任何東西
-
我們可以輕鬆地使用 Node.js 內部組件
當然,儘管有這些優點,但是 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