前端視角解讀 Why Rust
爲什麼要學 Rust
因爲我們需要使用合適的工具解決合適的問題
目前 Rust 對 WebAssembly 的支持是最好的,對於前端開發來說,可以將 CPU 密集型的 JavaScript 邏輯用 Rust 重寫,然後再用 WebAssembly 來運行,JavaScript 和 Rust 的結合將會讓你獲得駕馭一切的力量。
但是 Rust 被公認是很難學的語言,學習曲線很陡峭。(學不動了
對於前端而言,所需要經歷的思維轉變會比其他語言更多。從命令式(imperative)編程語言轉換到函數式(functional)編程語言、從變量的可變性(mutable)遷移到不可變性(immutable)、從弱類型語言遷移到強類型語言,以及從手工或者自動內存管理到通過生命週期來管理內存,難度逐級遞增。
而當我們邁過了這些思維轉變後,會發現 Rust 的確有過人之處:
-
從內核來看,它重塑了我們對一些基本概念的理解。比如 Rust 清晰地定義了變量在一個作用域下的生命週期,讓開發者在摒棄垃圾回收(GC)前提下,還能夠無需關心手動內存管理,讓內存安全和高性能二者兼得。
-
從外觀來看,它使用起來感覺很像 Python/TypeScript 這樣的高級語言,表達能力一流,但性能絲毫不輸於 C/C++,從而讓表達力和高性能二者兼得。
-
擁有友好的編譯器和清晰明確的錯誤提示與完整的文檔,基本可以做到只要編譯通過,即可上線。
大概瞭解這些後,那我們開始從幾個簡單的 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(value: i32, f: fn(i32) -> i32) -> i32 {
f(value)
}
// 入參和返回類型爲i32(有符號,大小在[-2^31, 2^31 - 1]範圍內的數字類型)
fn square(value: i32) -> i32 {
// 沒有寫;代表直接返回,相當於 return value * value;
value * value
}
fn cube(value: i32) -> 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(coin: Coin) -> u8 {
// 使用 match 進行類型匹配
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
先聊聊堆和棧
我們在寫 js 的時候,似乎不需要特別關注堆和棧以及內存的分配,js 會幫忙我們 “自動” 搞定一切。但這個 “自動” 正是一切混亂的根源,讓我們錯誤的感覺我們可以不關心內存管理。
我們重新回過來看一看這些基礎知識,以及 Rust 是怎麼處理內存管理的。
棧空間
棧的特點是 “LIFO,即後進先出” 。數據存儲時只能從頂部逐個存入,取出時也需從頂部逐個取出。比如一個乒乓球的盒子,先放進去(入棧)的乒乓球就只能後出來(出棧)。
在每次調用函數,都會在棧的頂端創建一個棧幀,用來保存該函數的上下文數據。比如該函數內部聲明的局部變量通常會保存在棧幀中。當該函數返回時,函數返回值也保留在該棧幀中。當函數調用者從棧幀中取得該函數返回值後,該棧幀被釋放。
堆空間
不同於棧空間由操作系統跟蹤管理,堆的特點是 無序 的key-value
鍵值對 存儲方式。
堆是在程序運行時,而不是在程序編譯時,申請某個大小的內存空間。即動態分配內存,對其訪問和對一般內存的訪問沒有區別。對於堆,我們可以隨心所欲的進行增加變量和刪除變量,不用遵循次序。
可以這麼總結:
-
棧適合存放存活時間短的數據。
-
數據要存放於棧中,要求數據所屬數據類型的大小是已知的。
-
使用棧的效率要高於使用堆。
對於存入棧上的值,它的大小在編譯期就需要確定。棧上存儲的變量生命週期在當前調用棧的作用域內,無法跨調用棧引用。
堆可以存入大小未知或者動態伸縮的數據類型。堆上存儲的變量,其生命週期從分配後開始,一直到釋放時才結束,因此堆上的變量允許在多個調用棧之間引用。
可以將棧理解爲將物品放進大小合適的紙箱並將紙箱按規律放進儲物間,堆理解爲在儲物間隨便找一個空位置來放置物品。顯然,以紙箱爲單位來存取物品的效率要高的多,而直接將物品放進凌亂的儲物間的效率要低的多,而且儲物間隨意堆放的東西越多,空閒位置就越零碎,存取物品的效率就越低,且空間利用率就越低。
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 的系統來說,這不是問題,v
和 v2
都引用同一個堆中的引用,最終由 GC 來回收就是了。
對於沒有 GC 的 Rust 而言,自然有它的辦法,那就是所有權特性中的 move 語義,這個我們在後面會講到。
Rust 語言特性
所有權和生命週期的存在使 Rust 成爲內存安全、沒有 GC 的高效語言。
所有權:掌控值的生死大權
計算機的內存資源非常寶貴,所有的程序運行的時候都需要某種方式來合理地利用計算機的內存資源,我們再看一下常見的幾種語言是如何利用內存的:
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(data: Vec<u32>, v: u32) -> Option<usize> {
for (pos, item) in data.iter().enumerate() {
// 解除 item 的引用,可以訪問到 item 的具體值
if *item == v {
return Some(pos);
}
}
None
}
Option 是 Rust 的系統類型,它是一個枚舉,包含了 Some 和 None,用來表示值不存在的可能,這在編程中是一個好的實踐,它強制 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:派生,編譯器可以通過 derive 爲 trait 加上一些基本實現,如
-
Copy[4]:使類型具有 “複製語義” 而非 “移動語義”。
-
Clone[5]:可以明確地創建一個值的深拷貝,在使用 Copy 的派生時一般需要把 Clone 加上,因爲 Clone 是 Copy 的超集。
-
Debug[6]:使用
{:?}
可以完整地打印當前值。
回到 v
參數的那個問題,因爲 v
是 u32
類型實現了 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(data: Vec<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 中,值的生命週期可分爲:
-
靜態生命週期:如果一個值的生命週期貫穿整個進程的生命週期,那麼我們就稱這種生命週期爲靜態生命週期。
-
**動態生命週期:**如果一個值是在某個作用域中定義的,也就是說它被創建在棧上或者堆上,那麼其生命週期是動態的。
-
分配在堆和棧上的內存有其各自的作用域,生命週期是動態的。
-
全局變量、靜態變量、字符串字面量、代碼等內容,在編譯時,會被編譯到可執行文件中,加載入內存。生命週期和進程的生命週期一致,生命週期是靜態的。
-
函數指針的生命週期也是靜態的,因爲函數在 Text 段中,只要進程活着,其內存一直存在。
有了這些概念,我們再來看一個例子:
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 和返回值的生命週期。
編譯器也給了我們解決方法:手動添加生命週期註釋,來告訴編譯器 s1
和 s2
的生命週期。
fn max<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1 > s2 {
s1
} else {
s2
}
}
這個例子或許大家看起來會很疑惑,s1
和 s2
的生命週期明明一致,爲什麼編譯器會無法判斷他們的生命週期呢?
其實很簡單,剛剛我們提到過,字符串字面量的生命週期是靜態的,而 s1 是動態的,它們的生命週期是不一致的。
當出現多個參數的時候,它們的生命週期不一致,返回的值的生命週期自然也不好確定,所以這個時候,我們需要進行生命週期標註,告訴編譯器這些引用間生命週期的約束。
Rust 與 Webassembly
WebAssembly(wasm)可以在現代的網絡瀏覽器中運行——它是一種低級的類彙編語言,具有緊湊的二進制格式,可以接近原生的性能運行。
簡而言之,對於網絡平臺而言,WebAssembly 它提供了一條途徑,以使得以各種語言編寫的代碼都可以以接近原生的速度在 Web 中運行。
對於前端而言,wasm 技術幫助解決一些場景下的性能瓶頸。
比如在瀏覽器中:
-
運行 VR、圖像視頻編輯、3D 遊戲
-
可以更好的讓一些語言和工具(如 AutoCAD)可以編譯到 Web 平臺
-
語言編譯器或虛擬機等
脫離瀏覽器的情況下:
-
遊戲分發服務(便攜、安全)
-
服務端執行不可信任的代碼。
-
服務端應用
-
移動混合原生應用
接下來,我們從一個圖片處理的例子入手,看一下如何從零構建一個 web-wasm 應用。
環境準備
Rust[7]
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
在安裝 Rustup 時,也會安裝 Rust 構建工具和包管理器的最新穩定版,即 Cargo。
Cargo 可以做很多事情,如:
-
cargo build
可以構建項目 -
cargo run
可以運行項目 -
cargo test
可以測試項目 -
cargo doc
可以爲項目構建文檔 -
cargo publish
可以將庫發佈到 crates.io[8]。
很明顯,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
-
支持 rust 包文件熱更新,監聽
src
目錄和Cargo.toml
文件變更,自動構建 -
vite 啓動優化,如果之前構建過,再次啓動
npm run dev
,則會跳過wasm-pack
構建
快速開始
創建一個 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',
]
-
dependencies
:依賴列表 -
package
:對於包的定義 -
lib
:我們當前是屬於庫工程(src 下是 lib.rs )而不是可執行工程(src 下是 main.rs),需要對其進行額外設置。 -
rlib:Rust Library 特定靜態中間庫格式。如果只是純 Rust 代碼項目之間的依賴和調用,那麼,用 rlib 就能完全滿足使用需求(默認)。
-
cdylib:c 規範的動態庫,它可以公開了 FFI 的一些功能,並可以被其他語言所調用。
添加 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 ALLOC: wee_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(_img: DynamicImage) -> 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_src: String) -> 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