前端視角解讀 Why Rust

爲什麼要學 Rust

因爲我們需要使用合適的工具解決合適的問題

目前 Rust 對 WebAssembly 的支持是最好的,對於前端開發來說,可以將 CPU 密集型的 JavaScript 邏輯用 Rust 重寫,然後再用 WebAssembly 來運行,JavaScript 和 Rust 的結合將會讓你獲得駕馭一切的力量。

但是 Rust 被公認是很難學的語言,學習曲線很陡峭。(學不動了

對於前端而言,所需要經歷的思維轉變會比其他語言更多。從命令式(imperative)編程語言轉換到函數式(functional)編程語言、從變量的可變性(mutable)遷移到不可變性(immutable)、從弱類型語言遷移到強類型語言,以及從手工或者自動內存管理到通過生命週期來管理內存,難度逐級遞增。

而當我們邁過了這些思維轉變後,會發現 Rust 的確有過人之處:

大概瞭解這些後,那我們開始從幾個簡單的 Rust demo 開始吧~

Ps:這篇文章並不能帶你直接掌握或者入門 Rust,並不會涉及到過多 api 講解,如有需求可直接跳轉文末參考資料。

Rust 初體驗

可使用 Rust Playground[1] 快速體驗

Hello World

// main()函數在獨立可執行文件中是不可或缺的,是程序的入口
fn main() {
    // 創建String類型的字符串字面量,使用 let 創建的默認是不可變的
    let target = String::from("rust");
    // println!()是一個宏,用於將參數輸出到 STDOUT
    println!("Hello World: {}", target);
}

函數抽象示例

fn apply(valuei32, ffn(i32) -> i32) -> i32 {
    f(value)
}

// 入參和返回類型爲i32(有符號,大小在[-2^31, 2^31 - 1]範圍內的數字類型)
fn square(valuei32) -> i32 {
    // 沒有寫;代表直接返回,相當於 return value * value;
    value * value
}

fn cube(valuei32) -> i32 {
    value * value * value
}

fn main() {
    // js中相當於console.log(`apply square: ${apply(2, square)}`)
    println!("apply square: {}", apply(2, square));
    println!("apply cube: {}", apply(2, cube));
}

控制流與枚舉

// 4種硬幣的值都屬於 Coin 類型
enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coinCoin) -> u8 {
    // 使用 match 進行類型匹配
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

先聊聊堆和棧

我們在寫 js 的時候,似乎不需要特別關注堆和棧以及內存的分配,js 會幫忙我們 “自動” 搞定一切。但這個 “自動” 正是一切混亂的根源,讓我們錯誤的感覺我們可以不關心內存管理。

我們重新回過來看一看這些基礎知識,以及 Rust 是怎麼處理內存管理的。

棧空間

棧的特點是 “LIFO,即後進先出” 。數據存儲時只能從頂部逐個存入,取出時也需從頂部逐個取出。比如一個乒乓球的盒子,先放進去(入棧)的乒乓球就只能後出來(出棧)。

在每次調用函數,都會在棧的頂端創建一個棧幀,用來保存該函數的上下文數據。比如該函數內部聲明的局部變量通常會保存在棧幀中。當該函數返回時,函數返回值也保留在該棧幀中。當函數調用者從棧幀中取得該函數返回值後,該棧幀被釋放。

堆空間

不同於棧空間由操作系統跟蹤管理,堆的特點是 無序key-value 鍵值對 存儲方式。

堆是在程序運行時,而不是在程序編譯時,申請某個大小的內存空間。即動態分配內存,對其訪問和對一般內存的訪問沒有區別。對於堆,我們可以隨心所欲的進行增加變量和刪除變量,不用遵循次序。

可以這麼總結:

  1. 棧適合存放存活時間短的數據。

  2. 數據要存放於棧中,要求數據所屬數據類型的大小是已知的。

  3. 使用棧的效率要高於使用堆。

對於存入棧上的值,它的大小在編譯期就需要確定。棧上存儲的變量生命週期在當前調用棧的作用域內,無法跨調用棧引用。

堆可以存入大小未知或者動態伸縮的數據類型。堆上存儲的變量,其生命週期從分配後開始,一直到釋放時才結束,因此堆上的變量允許在多個調用棧之間引用。

可以將棧理解爲將物品放進大小合適的紙箱並將紙箱按規律放進儲物間,堆理解爲在儲物間隨便找一個空位置來放置物品。顯然,以紙箱爲單位來存取物品的效率要高的多,而直接將物品放進凌亂的儲物間的效率要低的多,而且儲物間隨意堆放的東西越多,空閒位置就越零碎,存取物品的效率就越低,且空間利用率就越低。

Rust 如何使用堆和棧

問題來了,我們先看看 JavaScript 是如何使用堆和棧的

JavaScript 中的內存也分爲棧內存和堆內存。一般來說:

Rust 中各種類型的值默認都存儲在棧中,除非顯式地使用Box::new()將它們存放在堆上。對於動態大小的類型 (如 Vec、String),則數據部分分佈在堆中,並在棧中留下胖指針指向實際的數據,棧中的那個胖指針結構是靜態大小的。

在堆與棧的使用中,各個語言看起來是差不多的,主要區別在於 GC 上。

在 JavaScript 的 GC 中,因爲沒有一些高級語言所擁有的垃圾回收器,js 自動尋找是否一些內存 “不再需要” 是很難判定的。因此,js 的垃圾回收實現只能有限制的解決一般問題。

比如現在對於引用的垃圾回收,使用的標記 - 清除算法 [2],仍然會存在那些無法從根對象查詢到的對象都將被清除的限制(儘管這是一個限制,但實踐中我們很少會碰到類似的情況,所以開發者不太會去關心垃圾回收機制)。

而 Rust 不同於其他的高級語言,它沒有提供 GC,也無需手動申請和手動釋放堆內存,但 Rust 可以保證我們當前的內存是安全的,即不會出現懸空指針等問題。其中一個原因是因爲 Rust 使用了自己的一套內存管理機制:Rust 中所有的大括號都是一個獨立的作用域,作用域內的變量在離開作用域時會失效,而變量綁定的數據 (無論是堆內還是棧中數據) 則自動被釋放

fn main() {
    // 每個大括號都是獨立的作用域
    {
        let n = 33;
        println!("{}", n);
    }
    // 變量n在這個時候失效
    // println!("{}", n);  // 編譯錯誤
}

那如果碰到這種情況呢:

fn main() {
    let v = vec![1, 2, 3];
    println!("{}", v[0]);
}

v 變量本身分配在棧中,用一個胖指針指向了堆中 v 裏的三個元素。當函數退出後,v 的作用域結束了,它所引用的堆中的元素也會被自動回收,聽起來不錯。

但問題來了,如果想要將 v 的值綁定在另一個變量 v2 上,會出現什麼情況呢?

對於有 GC 的系統來說,這不是問題,vv2 都引用同一個堆中的引用,最終由 GC 來回收就是了。

對於沒有 GC 的 Rust 而言,自然有它的辦法,那就是所有權特性中的 move 語義,這個我們在後面會講到。

Rust 語言特性

所有權和生命週期的存在使 Rust 成爲內存安全、沒有 GC 的高效語言。

所有權:掌控值的生死大權

計算機的內存資源非常寶貴,所有的程序運行的時候都需要某種方式來合理地利用計算機的內存資源,我們再看一下常見的幾種語言是如何利用內存的:

cycj2y

Rust 的流行和受歡迎是因爲它可以在不使用垃圾收集的同時保證內存安全。而其它諸如 JavaScript、Go 等語言則是使用垃圾收集來做內存管理,垃圾收集器以資源和性能爲代價爲開發人員提供了方便,但是一旦碰到問題,就會很難排查。在 rust 世界裏,當你嚴格遵循規則的時候,就可以拋開垃圾收集實現內存安全。

我們先從一個變量使用堆棧的行爲開始,探究 Rust 設計所有權和生命週期的用意。

變量在函數調用時發生了什麼

我們先來看一段代碼:

fn main() {
    // 定義一個動態數組
    let data = vec![10, 42, 9, 8];
    let v = 42;
    // 使用 if let 進行模式匹配。
    // 它和直接 if 判斷的區別是 if 匹配的是布爾值而 if let 匹配的是模式
    if let Some(pos) = find_pos(data, v) {
        println!("Found {} at {}", v, pos);
    }
}

// 在 data 中查找 v 是否存在,存在則返回 v 在 data 中的下標,不存在返回 None
fn find_pos(dataVec<u32>, vu32) -> Option<usize> {
    for (pos, item) in data.iter().enumerate() {
        // 解除 item 的引用,可以訪問到 item 的具體值
        if *item == v {
            return Some(pos);
        }
    }

    None
}

Option 是 Rust 的系統類型,它是一個枚舉,包含了 SomeNone,用來表示值不存在的可能,這在編程中是一個好的實踐,它強制 Rust 檢測和處理值不存在的情況。

這段代碼不難理解,要再強調一下的是,動態數組因爲大小在編譯期無法確定,所以放在堆上,並且在棧上有一個包含了長度和容量的胖指針指向堆上的內存。

在調用 find_pos() 時,main() 函數中的局部變量 data 和 v 作爲參數傳遞給了 find_pos(),所以它們會被放在 find_pos() 的參數區。

按照大多數編程語言的做法,現在堆上的內存就有了兩個引用。不光如此,我們每把 data 作爲參數傳遞一次,堆上的內存就會多一次引用。

但是,這些引用究竟會做什麼操作,我們不得而知,也無從限制;而且堆上的內存究竟什麼時候能釋放,尤其在多個調用棧引用時,很難釐清,取決於最後一個引用什麼時候結束。所以,這樣一個看似簡單的函數調用,給內存管理帶來了極大麻煩。

所有權和 Move 語義

在 Rust 的所有權規則下,上述的問題將不再是問題,所有權規則可以總結:

在所有權的規則下,我們看一下上述的引用問題是如何解決的:

原先 main() 函數中的 data,被移動到 find_pos() 後,就失效了,編譯器會保證 main() 函數隨後的代碼無法訪問這個變量,這樣,就確保了堆上的內存依舊只有唯一的引用。

但爲什麼 v 沒有被移動反而依舊被複制了呢?一會你就明白了。

所以在所有權規則下,解決了誰真正擁有值的生死大權問題,讓堆上數據的多重引用不復存在,這是它最大的優勢。

但是很明顯它也產生了一些問題,最大的一個就是會讓代碼變得很複雜,尤其是一些只存儲在棧上的簡單數據,如果要避免所有權轉移之後不能訪問的情況,我們就需要調用 clone() 來進行復制,這樣效率也不會很高。

Rust 考慮到了這一點,所以在 Move 語義之外,Rust 還提供了 Copy 語義。如果一個數據結構實現了 Copy trait[3],那麼它就會使用 Copy 語義。這樣,在你賦值或者傳參時,值會自動按位拷貝(淺拷貝)。

#[derive(Debug)]
struct Foo;

let x = Foo;
let y = x; // unused variable: `y`

// `x` has moved into `y`, and so cannot be used
println!("{:?}", x); // error: use of moved value
#[derive(Debug, Copy, Clone)]
struct Foo;

let x = Foo;
// 變量命名前加_代表這個變量處於 todo 狀態,編譯器會忽視 unused 檢查
let _y = x;

// `y` is a copy of `x`
println!("{:?}", x); // A-OK!

struct:可以視爲 es6 中的 class,但建議還是將 struct 視作是純粹的數據。

trait:類似於接口,特性與接口相同的地方在於它們都是一種行爲規範,可以用於標識哪些類有哪些方法。

derive:派生,編譯器可以通過 derivetrait 加上一些基本實現,如

回到 v 參數的那個問題,因爲 vu32 類型實現了 Copy trait,且分配在棧上,調用 find_pos 時便會自動 Copy 了一份 v'

但如果我們不想使用 copy 語義,避免內存過多的被複制,我們可以使用 “借用” 數據

值的借用

我們來看新的一個例子:

fn main() {
    let data = vec![1, 2, 3, 4];
    let data1 = data;
    println!("sum of data1: {}", sum(data1));
    println!("data1: {:?}", data1); // error1
    println!("sum of data: {}", sum(data)); // error2
}

fn sum(dataVec<u32>) -> u32 {
    // 創建一個迭代器,fold 方法用法類似 reduce
    data.iter().fold(0, |acc, x| acc + x)
}

很明顯上述代碼無法通過編譯,data 和 data1 在執行賦值語句和執行 sum 方法的時候,所有權均被 move 過去了,在後面再調用他們自然會報錯。

編譯器也非常智能地提示了我們錯誤所在:

error[E0382]: borrow of moved value: `data1`
 --> src/main.rs:5:29
  |
3 |     let data1 = data;
  |         ----- move occurs because `data1` has type `Vec<u32>`, which does not implement the `Copy` trait
4 |     println!("sum of data1: {}", sum(data1));
  |                                      ----- value moved here
5 |     println!("data1: {:?}", data1); // error1
  |                             ^^^^^ value borrowed here after move

error[E0382]: use of moved value: `data`
 --> src/main.rs:6:37
  |
2 |     let data = vec![1, 2, 3, 4];
  |         ---- move occurs because `data` has type `Vec<u32>`, which does not implement the `Copy` trait
3 |     let data1 = data;
  |                 ---- value moved here
...
6 |     println!("sum of data: {}", sum(data)); // error2
  |                                     ^^^^ value used here after move

For more information about this error, try `rustc --explain E0382`.
error: could not compile `playground` due to 2 previous errors

但我們只需要這樣改一下,可以不使用 copy 的情況下通過編譯:

fn main() {
    let data = vec![1, 2, 3, 4];
    let data1 = &data;
    println!("sum of data1: {}", sum(&data1));
    println!("data1: {:?}", data1);
    println!("sum of data: {}", sum(&data));
}

fn sum(data&Vec<u32>) -> u32 {
    data.iter().fold(0, |acc, x| acc + x)
}

使用 & 可以來實現 Borrow 語義。顧名思義,Borrow 語義允許一個值的所有權,在不發生轉移的情況下,被其它上下文使用。

在 Rust 中,“借用”和 “引用” 是一個概念, 同時在 Rust 下,所有的引用都只是借用了 “臨時使用權”,它並不破壞值的單一所有權約束。

所以,默認情況下,Rust 的 “借用” 都是隻讀的

當然,我們對值的借用也得有一個限制:借用不能超過值的生命週期

生命週期我們熟悉,寫 React 或者 Vue 的時候,每個組件都有從創建到銷燬的生命週期,那在 Rust 裏,值的生命週期是怎麼樣的呢,值的借用限制什麼和生命週期有關呢,我們接着往下看。

生命週期:我們創建的值可以活多久

在任何語言裏,棧上的值都有自己的生命週期,它和幀的生命週期一致。

在 Rust 中,堆上的內存也引入生命週期的概念:除非顯式地做 Box::leak() 等動作,一般來說,堆內存的生命週期,會默認和其棧內存的生命週期綁定在一起。

Box:使用 Box<T> 允許你將一個值放在堆上而不是棧上,留在棧上的則是指向堆數據的指針。除了數據被儲存在堆上而不是棧上之外,box 沒有性能損失,它們多用於如下場景:

我們先來看一個例子:

fn main() {
    let r;

    {
        let x = 5;
        r = &x;
    }

    println!("r: {}", r);
}

這段代碼並不會通過編譯,因爲 r 所引用的值已經在使用之前被釋放。

error[E0597]: `x` does not live long enough
 --> src/main.rs:6:13
  |
6 |         r = &x;
  |             ^^ borrowed value does not live long enough
7 |     }
  |     - `x` dropped here while still borrowed
8 |
9 |     println!("r: {}", r);
  |                       - borrow later used here

Rust 編譯器有一個借用檢查器,它比較作用域來確保所有的借用都是有效的,比如上述例子,我們加上生命週期的註釋再看一下:

fn main() {
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {}", r); //          |
}                         // ---------+

如你所見,內部的 'b 塊要比外部的生命週期 'a 小得多。在編譯時,Rust 比較這兩個生命週期的大小,並發現 r 擁有生命週期 'a,不過它引用了一個擁有生命週期 'b 的對象。程序被拒絕編譯,因爲生命週期 'b 比生命週期 'a 要小:被引用的對象比它的引用者存在的時間更短。

由此,我們也解釋了 Rust 在值的借用中的那個規則:借用不能超過值的生命週期。

在 Rust 中,值的生命週期可分爲:

有了這些概念,我們再來看一個例子:

fn main() {
    let s1 = String::from("Lindsey");
    let s2 = String::from("Rosie");

    let result = max(&s1, &s2);

    println!("bigger one: {}", result);
}

fn max(s1&str, s2&str) -> &str {
    if s1 > s2 {
        s1
    } else {
        s2
    }
}

同樣,這段代碼也無法通過編譯,編譯器報錯信息如下:

error[E0106]: missing lifetime specifier
  --> src/main.rs:10:31
   |
10 | fn max(s1: &str, s2: &str) -> &str {
   |            ----      ----     ^ expected named lifetime parameter
   |
   = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `s1` or `s2`
help: consider introducing a named lifetime parameter
   |
10 | fn max<'a>(s1: &'a str, s2: &'a str) -> &'a str {
   |       ++++      ++           ++          ++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `playground` due to previous error

Rust 的編譯器始終是一個良師益友,十分嚴格且優秀,引導我寫出更可靠而高效的代碼。

missing lifetime specifier 意思是編譯器在編譯 max() 函數時,無法判斷 s1、s2 和返回值的生命週期。

編譯器也給了我們解決方法:手動添加生命週期註釋,來告訴編譯器 s1s2 的生命週期。

fn max<'a>(s1&'a str, s2&'a str) -> &'a str {
    if s1 > s2 {
        s1
    } else {
        s2
    }
}

這個例子或許大家看起來會很疑惑,s1s2 的生命週期明明一致,爲什麼編譯器會無法判斷他們的生命週期呢?

其實很簡單,剛剛我們提到過,字符串字面量的生命週期是靜態的,而 s1 是動態的,它們的生命週期是不一致的。

當出現多個參數的時候,它們的生命週期不一致,返回的值的生命週期自然也不好確定,所以這個時候,我們需要進行生命週期標註,告訴編譯器這些引用間生命週期的約束。

Rust 與 Webassembly

WebAssembly(wasm)可以在現代的網絡瀏覽器中運行——它是一種低級的類彙編語言,具有緊湊的二進制格式,可以接近原生的性能運行。

簡而言之,對於網絡平臺而言,WebAssembly 它提供了一條途徑,以使得以各種語言編寫的代碼都可以以接近原生的速度在 Web 中運行。

對於前端而言,wasm 技術幫助解決一些場景下的性能瓶頸。

比如在瀏覽器中:

脫離瀏覽器的情況下:

接下來,我們從一個圖片處理的例子入手,看一下如何從零構建一個 web-wasm 應用。

環境準備

Rust[7]

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

在安裝 Rustup 時,也會安裝 Rust 構建工具和包管理器的最新穩定版,即 Cargo。

Cargo 可以做很多事情,如:

很明顯,Cargo 在 Rust 中扮演的 Npm 在 Node 中的角色。

Wasm-pack[9]

curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

wasm-pack 用於構建和使用我們希望與 JavaScript,瀏覽器或 Node.js 互操作的 Rust 生成的 WebAssembly。

Vite[10]

這裏的前端構建工具我使用的是 Vite,在 Vite 中還需要增加一個插件:

vite-plugin-rsw[11]:集成了 wasm-pack 的 CLI

快速開始

創建一個 vite 項目

yarn create vite vite-webassembly

添加 vite-plugin-rsw 插件

yarn add vite-plugin-rsw -D

並增加相應配置:

// vite.config.ts

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import ViteRsw from 'vite-plugin-rsw'

export default defineConfig({
  plugins: [
    react(),
    // 查看更多:https://github.com/lencx/vite-plugin-rsw
    ViteRsw({
      // 如果包在`unLinks`和`crates`都配置過
      // 會執行,先卸載(npm unlink),再安裝(npm link)
      // 例如下面會執行
      // `npm unlink picture-wasm`
      unLinks: ['picture-wasm'],
      // 項目根路徑下的rust項目
      // `@`開頭的爲npm組織
      // 例如下面會執行:
      // `npm link picture-wasm`
      // 因爲執行順序原因,雖然上面的unLinks會把`picture-wasm`卸載
      // 但是這裏會重新進行安裝
      crates: [ picture-wasm ],
    }),
  ],
})

使用 cargo 初始化一個 rust 項目

在當前目錄下執行:

cargo new picture-wasm

我們先來看一下現在的目錄結構

[my-wasm-app] # 項目根路徑
|- [picture-wasm] # npm包 `wasm-hey`
|    |- [pkg] # 生成wasm包的目錄
|    |    |- picture-wasm_bg.wasm # wasm文件
|    |    |- picture-wasm.js # 包入口文件
|    |    |- picture-wasm_bg.wasm.d.ts # ts聲明文件
|    |    |- picture-wasm.d.ts # ts聲明文件
|    |    |- package.json
|    |    - ...
|    |- [src] rust源代碼
|    | # 瞭解更多: https://doc.rust-lang.org/cargo/reference/cargo-targets.html
|    |- [target] # 項目依賴,類似於npm的 `node_modules`
|    | # 瞭解更多: https://doc.rust-lang.org/cargo/reference/manifest.html
|    |- Cargo.toml # rust包管理清單
|    - ...
|- [node_modules] # 前端的項目包依賴
|- [src] # 前端源代碼(可以是vue, react, 或其他)
| # 瞭解更多: https://nodejs.dev/learn/the-package-json-guide
|- package.json # `yarn` 包管理清單
| # 瞭解更多: https://vitejs.dev/config
|- vite.config.ts # vite配置文件
| # 瞭解更多: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html
|- tsconfig.json # typescript配置文件

可以看到,生成的 picture-wasm 項目中,有一個 Cargo.toml 文件,它就類似於我們的 package.json ,是用作 Rust 的包管理的一個清單。

Cargo.toml 中增加配置

[package]
name = "picture-wasm"
version = "0.1.0"
edition = "2021"

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

[dependencies]
wasm-bindgen = "0.2.70"
base64 = "0.12.1"
image = { version = "0.23.4", default-features = false, features = ["jpeg", "png"] }
console_error_panic_hook = { version = "0.1.1", optional = true }
wee_alloc = { version = "0.4.2", optional = true }

[dependencies.web-sys]
version = "0.3.4"
features = [
  'Document',
  'Element',
  'HtmlElement',
  'Node',
  'Window',
]

添加 Rust 代碼

// picture-wasm/src/lib.rs

// 鏈接到 `image` 和 `base64` 庫,導入其中的項
extern crate image;
extern crate base64;
// 使用 `use` 從 image 的命名空間導入對應的方法
use image::DynamicImage;
use image::ImageFormat;
// 從 std(基礎庫)的命名空間導入對應方法,可用解構的方式
use std::io::{Cursor, Read, Seek, SeekFrom};
use std::panic;
use base64::{encode};

// 引入 wasm_bindgen 下 prelude 所有模塊,用作 在 Rust 與 JavaScript 之間通信
use wasm_bindgen::prelude::*;

// 當`wee_alloc`特性啓用的時候,使用`wee_alloc`作爲全局分配器。
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOCwee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

// #[wasm_bindgen] 屬性表明它下面的函數可以在JavaScript和Rust中訪問。
#[wasm_bindgen]
extern "C"  {
    // 該 extern 塊將外部 JavaScript 函數 console.log 導入 Rust。
    // 通過以這種方式聲明它,wasm-bindgen 將創建 JavaScript 存根 console
    // 允許我們在 Rust 和 JavaScript 之間來回傳遞字符串。
    #[wasm_bindgen(js_namespace = console)]
    fn log(s&str);
}

fn load_image_from_array(_array&[u8]) -> DynamicImage {
    // 使用 match 進行兜底報錯匹配
    let img = match image::load_from_memory_with_format(_array, ImageFormat::Png) {
        Ok(img) => img,
        Err(error) => {
            panic!("There was a problem opening the file: {:?}", error)
        }
    };
    img
}

fn get_image_as_base64(_imgDynamicImage) -> String {
    // 使用 mut 聲明可變變量,類似 js 中的 let,不使用 mut 爲不可變
    // 使用 Cursor 創建一個內存緩存區,裏面是動態數組類型
    let mut c = Cursor::new(Vec::new());
    // 寫入圖片
    match _img.write_to(&mut c, ImageFormat::Png) {
        Ok(c) => c,
        Err(error) => {
            panic!(
                "There was a problem writing the resulting buffer: {:?}",
                error
            )
        }
    };
    // 尋找以字節爲單位的偏移量,直接用 unwrap 隱式處理 Option 類型,直接返回值或者報錯
    c.seek(SeekFrom::Start(0)).unwrap();

    // 聲明一個可變的動態數組作輸出
    let mut out = Vec::new();
    c.read_to_end(&mut out).unwrap();
    // 使用 encode 轉換
    let stt = encode(&mut out);
    let together = format!("{}{}", "data:image/png;base64,", stt);
    together
}

#[wasm_bindgen]
pub fn grayscale(_array&[u8]) -> Result<(), JsValue> {
    let mut img = load_image_from_array(_array);
    img = img.grayscale();
    let base64_str = get_image_as_base64(img);
    append_img(base64_str)
}

pub fn append_img(image_srcString) -> Result<(), JsValue> {
    // 使用 `web_sys` 來獲取 window 對象
    let window = web_sys::window().expect("no global `window` exists");
    let document = window.document().expect("should have a document on window");
    let body = document.body().expect("document should have a body");

    // 創建 img 元素
    // 使用 `?` 在出現錯誤的時候會直接返回 Err
    let val = document.create_element("img")?;
    // val.set_inner_html("Hello from Rust!");
    val.set_attribute("src", &image_src)?;
    val.set_attribute("style", "height: 200px")?;
    body.append_child(&val)?;
    log("success!");

    Ok(())
}

React 項目中調用 Wasm 方法

// src/App.tsx

import React, { useEffect } from "react" ;
import init, { grayscale } from "picture-wasm" ;

import logo from "./logo.svg";
import "./App.css";

function App() {
  useEffect(() => {
    // wasm初始化,在調用`picture-wasm`包方法時
    // 必須先保證已經進行過初始化,否則會報錯
    // 如果存在多個wasm包,則必須對每一個wasm包進行初始化
    init();
  }, []);

  const fileImport = (e: any) => {
    const selectedFile = e.target.files[0];
    //獲取讀取我文件的File對象
    // var selectedFile = document.getElementById( files ).files[0];
    var reader = new FileReader(); //這是核心,讀取操作就是由它完成.
    reader.readAsArrayBuffer(selectedFile); //讀取文件的內容,也可以讀取文件的URL
    reader.onload = (res: any) => {
      var uint8Array = new Uint8Array(res.target.result as ArrayBuffer);
      grayscale(uint8Array);
    };
  };

  return (
    <div class>
      <header class>
        <img src={logo} class />
        <p>Hello WebAssembly!</p>
        <p>Vite + Rust + React</p>
        <input type="file" id="files" onChange={fileImport} />
      </header>
    </div>
  );
}

export default App;

執行!

在根目錄下執行 yarn dev ,rsw 插件會打包 rust 項目並軟鏈接過來,這樣一個本地彩色圖片轉換爲黑白圖片的 web-wasm 應用就完成了。

參考資料

[1] Rust Playground: https://play.rust-lang.org/

[2] 標記 - 清除算法: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Memory_Management# 垃圾回收

[3] Copy trait: https://doc.rust-lang.org/std/marker/trait.Copy.html

[4] Copy: https://rustwiki.org/zh-CN/core/marker/trait.Copy.html

[5] Clone: https://rustwiki.org/zh-CN/std/clone/trait.Clone.html

[6] Debug: https://rustwiki.org/zh-CN/std/fmt/trait.Debug.html

[7] Rust: https://www.rust-lang.org/zh-CN/

[8] crates.io: https://crates.io/

[9] Wasm-pack: https://github.com/rustwasm/wasm-pack

[10] Vite: https://cn.vitejs.dev/

[11] vite-plugin-rsw: https://github.com/lencx/vite-plugin-rsw

其他參考

陳天 · Rust 編程第一課

https://time.geekbang.org/column/article/408400

Rust 入門第一課

https://rust-book.junmajinlong.com/ch1/00.html

2021 年 Rust 行業調研報告 - InfoQ

https://www.infoq.cn/article/umqbighceoa81yij7uyg 24 days from node.js to Rust

**通過例子學 ** Rust

https://rustwiki.org/zh-CN/rust-by-example/hello.html

客戶端視角認識與感受 Rust 的紅與黑

https://tech.bytedance.net/articles/7036575152028516365

實現一個簡單的基於 WebAssembly 的圖片處理應用 https://juejin.cn/post/6844904205417709581

Rust 和 WebAssembly

https://rustmagazine.github.io/rust_magazine_2021/chapter_2/rust_wasm_frontend.html

24 days from node.js to Rust

24 days from node.js to Rust (vino.dev)

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