前端視角解讀 Why Rust
大廠技術(shù) 高級(jí)前端 精選文章
點(diǎn)擊上方 全站前端精選,關(guān)注公眾號(hào)
回復(fù)1,加入高級(jí)前段交流群
為什么要學(xué) Rust
因?yàn)槲覀冃枰褂煤线m的工具解決合適的問(wèn)題
目前 Rust 對(duì) WebAssembly 的支持是最好的,對(duì)于前端開(kāi)發(fā)來(lái)說(shuō),可以將 CPU 密集型的 JavaScript 邏輯用 Rust 重寫(xiě),然后再用 WebAssembly 來(lái)運(yùn)行,JavaScript 和 Rust 的結(jié)合將會(huì)讓你獲得駕馭一切的力量。
但是 Rust 被公認(rèn)是很難學(xué)的語(yǔ)言,學(xué)習(xí)曲線很陡峭。(學(xué)不動(dòng)了
對(duì)于前端而言,所需要經(jīng)歷的思維轉(zhuǎn)變會(huì)比其他語(yǔ)言更多。從命令式(imperative)編程語(yǔ)言轉(zhuǎn)換到函數(shù)式(functional)編程語(yǔ)言、從變量的可變性(mutable)遷移到不可變性(immutable)、從弱類(lèi)型語(yǔ)言遷移到強(qiáng)類(lèi)型語(yǔ)言,以及從手工或者自動(dòng)內(nèi)存管理到通過(guò)生命周期來(lái)管理內(nèi)存,難度逐級(jí)遞增。
而當(dāng)我們邁過(guò)了這些思維轉(zhuǎn)變后,會(huì)發(fā)現(xiàn) Rust 的確有過(guò)人之處:
從內(nèi)核來(lái)看,它重塑了我們對(duì)一些基本概念的理解。比如 Rust 清晰地定義了變量在一個(gè)作用域下的生命周期,讓開(kāi)發(fā)者在摒棄垃圾回收(GC)前提下,還能夠無(wú)需關(guān)心手動(dòng)內(nèi)存管理,讓內(nèi)存安全和高性能二者兼得。
從外觀來(lái)看,它使用起來(lái)感覺(jué)很像 Python/TypeScript 這樣的高級(jí)語(yǔ)言,表達(dá)能力一流,但性能絲毫不輸于 C/C++,從而讓表達(dá)力和高性能二者兼得。
擁有友好的編譯器和清晰明確的錯(cuò)誤提示與完整的文檔,基本可以做到只要編譯通過(guò),即可上線。
大概了解這些后,那我們開(kāi)始從幾個(gè)簡(jiǎn)單的 Rust demo 開(kāi)始吧~
Ps:這篇文章并不能帶你直接掌握或者入門(mén) Rust,并不會(huì)涉及到過(guò)多 api 講解,如有需求可直接跳轉(zhuǎn)文末參考資料。
Rust 初體驗(yàn)
可使用 Rust Playground[1] 快速體驗(yàn)
Hello World
// main()函數(shù)在獨(dú)立可執(zhí)行文件中是不可或缺的,是程序的入口
fn main() {
// 創(chuàng)建String類(lèi)型的字符串字面量,使用 let 創(chuàng)建的默認(rèn)是不可變的
let target = String::from("rust");
// println!()是一個(gè)宏,用于將參數(shù)輸出到 STDOUT
println!("Hello World: {}", target);
}
函數(shù)抽象示例
fn apply(value: i32, f: fn(i32) -> i32) -> i32 {
f(value)
}
// 入?yún)⒑头祷仡?lèi)型為i32(有符號(hào),大小在[-2^31, 2^31 - 1]范圍內(nèi)的數(shù)字類(lèi)型)
fn square(value: i32) -> i32 {
// 沒(méi)有寫(xiě);代表直接返回,相當(dāng)于 return value * value;
value * value
}
fn cube(value: i32) -> i32 {
value * value * value
}
fn main() {
// js中相當(dāng)于console.log(`apply square: ${apply(2, square)}`)
println!("apply square: {}", apply(2, square));
println!("apply cube: {}", apply(2, cube));
}
控制流與枚舉
// 4種硬幣的值都屬于 Coin 類(lèi)型
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
// 使用 match 進(jìn)行類(lèi)型匹配
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
先聊聊堆和棧
我們?cè)趯?xiě) js 的時(shí)候,似乎不需要特別關(guān)注堆和棧以及內(nèi)存的分配,js 會(huì)幫忙我們“自動(dòng)”搞定一切。但這個(gè)“自動(dòng)”正是一切混亂的根源,讓我們錯(cuò)誤的感覺(jué)我們可以不關(guān)心內(nèi)存管理。
我們重新回過(guò)來(lái)看一看這些基礎(chǔ)知識(shí),以及 Rust 是怎么處理內(nèi)存管理的。
??臻g
棧的特點(diǎn)是 “LIFO,即后進(jìn)先出” 。數(shù)據(jù)存儲(chǔ)時(shí)只能從頂部逐個(gè)存入,取出時(shí)也需從頂部逐個(gè)取出。比如一個(gè)乒乓球的盒子,先放進(jìn)去(入棧)的乒乓球就只能后出來(lái)(出棧)。
在每次調(diào)用函數(shù),都會(huì)在棧的頂端創(chuàng)建一個(gè)棧幀,用來(lái)保存該函數(shù)的上下文數(shù)據(jù)。比如該函數(shù)內(nèi)部聲明的局部變量通常會(huì)保存在棧幀中。當(dāng)該函數(shù)返回時(shí),函數(shù)返回值也保留在該棧幀中。當(dāng)函數(shù)調(diào)用者從棧幀中取得該函數(shù)返回值后,該棧幀被釋放。
堆空間
不同于??臻g由操作系統(tǒng)跟蹤管理,堆的特點(diǎn)是 無(wú)序 的key-value 鍵值對(duì) 存儲(chǔ)方式。
堆是在程序運(yùn)行時(shí),而不是在程序編譯時(shí),申請(qǐng)某個(gè)大小的內(nèi)存空間。即動(dòng)態(tài)分配內(nèi)存,對(duì)其訪問(wèn)和對(duì)一般內(nèi)存的訪問(wèn)沒(méi)有區(qū)別。對(duì)于堆,我們可以隨心所欲的進(jìn)行增加變量和刪除變量,不用遵循次序。
可以這么總結(jié):
棧適合存放存活時(shí)間短的數(shù)據(jù)。 數(shù)據(jù)要存放于棧中,要求數(shù)據(jù)所屬數(shù)據(jù)類(lèi)型的大小是已知的。 使用棧的效率要高于使用堆。
對(duì)于存入棧上的值,它的大小在編譯期就需要確定。棧上存儲(chǔ)的變量生命周期在當(dāng)前調(diào)用棧的作用域內(nèi),無(wú)法跨調(diào)用棧引用。
堆可以存入大小未知或者動(dòng)態(tài)伸縮的數(shù)據(jù)類(lèi)型。堆上存儲(chǔ)的變量,其生命周期從分配后開(kāi)始,一直到釋放時(shí)才結(jié)束,因此堆上的變量允許在多個(gè)調(diào)用棧之間引用。
可以將棧理解為將物品放進(jìn)大小合適的紙箱并將紙箱按規(guī)律放進(jìn)儲(chǔ)物間,堆理解為在儲(chǔ)物間隨便找一個(gè)空位置來(lái)放置物品。顯然,以紙箱為單位來(lái)存取物品的效率要高的多,而直接將物品放進(jìn)凌亂的儲(chǔ)物間的效率要低的多,而且儲(chǔ)物間隨意堆放的東西越多,空閑位置就越零碎,存取物品的效率就越低,且空間利用率就越低。

Rust 如何使用堆和棧
問(wèn)題來(lái)了,我們先看看 JavaScript 是如何使用堆和棧的
JavaScript 中的內(nèi)存也分為棧內(nèi)存和堆內(nèi)存。一般來(lái)說(shuō):
棧內(nèi)存中存放的是存儲(chǔ)對(duì)象的地址;
而堆內(nèi)存中存放的是存儲(chǔ)對(duì)象的具體內(nèi)容。
對(duì)于原始類(lèi)型的值而言,其地址和具體內(nèi)容都存在于棧內(nèi)存中;
而基于引用類(lèi)型的值,其地址存在棧內(nèi)存,其具體內(nèi)容存在堆內(nèi)存中。

Rust 中各種類(lèi)型的值默認(rèn)都存儲(chǔ)在棧中,除非顯式地使用Box::new()將它們存放在堆上。對(duì)于動(dòng)態(tài)大小的類(lèi)型 (如 Vec、String),則數(shù)據(jù)部分分布在堆中,并在棧中留下胖指針指向?qū)嶋H的數(shù)據(jù),棧中的那個(gè)胖指針結(jié)構(gòu)是靜態(tài)大小的。
在堆與棧的使用中,各個(gè)語(yǔ)言看起來(lái)是差不多的,主要區(qū)別在于 GC 上。
在 JavaScript 的 GC 中,因?yàn)闆](méi)有一些高級(jí)語(yǔ)言所擁有的垃圾回收器,js 自動(dòng)尋找是否一些內(nèi)存“不再需要”是很難判定的。因此,js 的垃圾回收實(shí)現(xiàn)只能有限制的解決一般問(wèn)題。
比如現(xiàn)在對(duì)于引用的垃圾回收,使用的標(biāo)記-清除算法[2],仍然會(huì)存在那些無(wú)法從根對(duì)象查詢(xún)到的對(duì)象都將被清除的限制(盡管這是一個(gè)限制,但實(shí)踐中我們很少會(huì)碰到類(lèi)似的情況,所以開(kāi)發(fā)者不太會(huì)去關(guān)心垃圾回收機(jī)制)。
而 Rust 不同于其他的高級(jí)語(yǔ)言,它沒(méi)有提供 GC,也無(wú)需手動(dòng)申請(qǐng)和手動(dòng)釋放堆內(nèi)存,但 Rust 可以保證我們當(dāng)前的內(nèi)存是安全的,即不會(huì)出現(xiàn)懸空指針等問(wèn)題。其中一個(gè)原因是因?yàn)?Rust 使用了自己的一套內(nèi)存管理機(jī)制:Rust 中所有的大括號(hào)都是一個(gè)獨(dú)立的作用域,作用域內(nèi)的變量在離開(kāi)作用域時(shí)會(huì)失效,而變量綁定的數(shù)據(jù)(無(wú)論是堆內(nèi)還是棧中數(shù)據(jù))則自動(dòng)被釋放。
fn main() {
// 每個(gè)大括號(hào)都是獨(dú)立的作用域
{
let n = 33;
println!("{}", n);
}
// 變量n在這個(gè)時(shí)候失效
// println!("{}", n); // 編譯錯(cuò)誤
}
那如果碰到這種情況呢:
fn main() {
let v = vec![1, 2, 3];
println!("{}", v[0]);
}
v 變量本身分配在棧中,用一個(gè)胖指針指向了堆中 v 里的三個(gè)元素。當(dāng)函數(shù)退出后,v 的作用域結(jié)束了,它所引用的堆中的元素也會(huì)被自動(dòng)回收,聽(tīng)起來(lái)不錯(cuò)。
但問(wèn)題來(lái)了,如果想要將 v 的值綁定在另一個(gè)變量 v2 上,會(huì)出現(xiàn)什么情況呢?
對(duì)于有 GC 的系統(tǒng)來(lái)說(shuō),這不是問(wèn)題,v 和 v2 都引用同一個(gè)堆中的引用,最終由 GC 來(lái)回收就是了。
對(duì)于沒(méi)有 GC 的 Rust 而言,自然有它的辦法,那就是所有權(quán)特性中的 move 語(yǔ)義,這個(gè)我們?cè)诤竺鏁?huì)講到。
Rust 語(yǔ)言特性
所有權(quán)和生命周期的存在使 Rust 成為內(nèi)存安全、沒(méi)有 GC 的高效語(yǔ)言。
所有權(quán):掌控值的生死大權(quán)
計(jì)算機(jī)的內(nèi)存資源非常寶貴,所有的程序運(yùn)行的時(shí)候都需要某種方式來(lái)合理地利用計(jì)算機(jī)的內(nèi)存資源,我們?cè)倏匆幌鲁R?jiàn)的幾種語(yǔ)言是如何利用內(nèi)存的:
語(yǔ)言 | 內(nèi)存使用方案 |
Java、Go | 垃圾回收機(jī)制,不停地查看一些內(nèi)存是否沒(méi)有在使用了,如果不再需要就將其釋放,占用更多的內(nèi)存和CPU資源 |
C、C++ | 程序員自己手動(dòng)申請(qǐng)和釋放內(nèi)存,容易出錯(cuò)且難以排查 |
JavaScript | 在創(chuàng)建變量(對(duì)象,字符串等)時(shí)自動(dòng)進(jìn)行了分配內(nèi)存,并且在不使用它們時(shí)“自動(dòng)”釋放。這個(gè)“自動(dòng)”是混亂的根源,并讓 JavaScript(和其他高級(jí)語(yǔ)言)開(kāi)發(fā)者錯(cuò)誤的感覺(jué)他們可以不關(guān)心內(nèi)存管理。 |
Rust | 所有權(quán)機(jī)制,內(nèi)存由所有權(quán)系統(tǒng)根據(jù)一系列的規(guī)則來(lái)管理,這些規(guī)則只會(huì)在程序編譯期間檢查 |
Rust 的流行和受歡迎是因?yàn)樗梢栽诓皇褂美占耐瑫r(shí)保證內(nèi)存安全。而其它諸如 JavaScript、Go 等語(yǔ)言則是使用垃圾收集來(lái)做內(nèi)存管理,垃圾收集器以資源和性能為代價(jià)為開(kāi)發(fā)人員提供了方便,但是一旦碰到問(wèn)題,就會(huì)很難排查。在 rust 世界里,當(dāng)你嚴(yán)格遵循規(guī)則的時(shí)候,就可以拋開(kāi)垃圾收集實(shí)現(xiàn)內(nèi)存安全。
我們先從一個(gè)變量使用堆棧的行為開(kāi)始,探究 Rust 設(shè)計(jì)所有權(quán)和生命周期的用意。
變量在函數(shù)調(diào)用時(shí)發(fā)生了什么
我們先來(lái)看一段代碼:
fn main() {
// 定義一個(gè)動(dòng)態(tài)數(shù)組
let data = vec![10, 42, 9, 8];
let v = 42;
// 使用 if let 進(jìn)行模式匹配。
// 它和直接 if 判斷的區(qū)別是 if 匹配的是布爾值而 if let 匹配的是模式
if let Some(pos) = find_pos(data, v) {
println!("Found {} at {}", v, pos);
}
}
// 在 data 中查找 v 是否存在,存在則返回 v 在 data 中的下標(biāo),不存在返回 None
fn find_pos(data: Vec<u32>, v: u32) -> Option<usize> {
for (pos, item) in data.iter().enumerate() {
// 解除 item 的引用,可以訪問(wèn)到 item 的具體值
if *item == v {
return Some(pos);
}
}
None
}
Option 是 Rust 的系統(tǒng)類(lèi)型,它是一個(gè)枚舉,包含了 Some 和 None,用來(lái)表示值不存在的可能,這在編程中是一個(gè)好的實(shí)踐,它強(qiáng)制 Rust 檢測(cè)和處理值不存在的情況。
這段代碼不難理解,要再?gòu)?qiáng)調(diào)一下的是,動(dòng)態(tài)數(shù)組因?yàn)榇笮≡诰幾g期無(wú)法確定,所以放在堆上,并且在棧上有一個(gè)包含了長(zhǎng)度和容量的胖指針指向堆上的內(nèi)存。
在調(diào)用 find_pos() 時(shí),main() 函數(shù)中的局部變量 data 和 v 作為參數(shù)傳遞給了 find_pos(),所以它們會(huì)被放在 find_pos() 的參數(shù)區(qū)。

按照大多數(shù)編程語(yǔ)言的做法,現(xiàn)在堆上的內(nèi)存就有了兩個(gè)引用。不光如此,我們每把 data 作為參數(shù)傳遞一次,堆上的內(nèi)存就會(huì)多一次引用。
但是,這些引用究竟會(huì)做什么操作,我們不得而知,也無(wú)從限制;而且堆上的內(nèi)存究竟什么時(shí)候能釋放,尤其在多個(gè)調(diào)用棧引用時(shí),很難厘清,取決于最后一個(gè)引用什么時(shí)候結(jié)束。所以,這樣一個(gè)看似簡(jiǎn)單的函數(shù)調(diào)用,給內(nèi)存管理帶來(lái)了極大麻煩。
所有權(quán)和 Move 語(yǔ)義
在 Rust 的所有權(quán)規(guī)則下,上述的問(wèn)題將不再是問(wèn)題,所有權(quán)規(guī)則可以總結(jié):
一個(gè)值只能被一個(gè)變量所擁有,這個(gè)變量被稱(chēng)為所有者。
一個(gè)值同一時(shí)刻只能有一個(gè)所有者。
當(dāng)所有者離開(kāi)作用域,其擁有的值被丟棄,內(nèi)存得到釋放。
在所有權(quán)的規(guī)則下,我們看一下上述的引用問(wèn)題是如何解決的:
原先 main() 函數(shù)中的 data,被移動(dòng)到 find_pos() 后,就失效了,編譯器會(huì)保證 main() 函數(shù)隨后的代碼無(wú)法訪問(wèn)這個(gè)變量,這樣,就確保了堆上的內(nèi)存依舊只有唯一的引用。
但為什么 v 沒(méi)有被移動(dòng)反而依舊被復(fù)制了呢?一會(huì)你就明白了。
所以在所有權(quán)規(guī)則下,解決了誰(shuí)真正擁有值的生死大權(quán)問(wèn)題,讓堆上數(shù)據(jù)的多重引用不復(fù)存在,這是它最大的優(yōu)勢(shì)。
但是很明顯它也產(chǎn)生了一些問(wèn)題,最大的一個(gè)就是會(huì)讓代碼變得很復(fù)雜,尤其是一些只存儲(chǔ)在棧上的簡(jiǎn)單數(shù)據(jù),如果要避免所有權(quán)轉(zhuǎn)移之后不能訪問(wèn)的情況,我們就需要調(diào)用 clone() 來(lái)進(jìn)行復(fù)制,這樣效率也不會(huì)很高。
Rust 考慮到了這一點(diǎn),所以在 Move 語(yǔ)義之外,Rust 還提供了 Copy 語(yǔ)義。如果一個(gè)數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)了 Copy trait[3],那么它就會(huì)使用 Copy 語(yǔ)義。這樣,在你賦值或者傳參時(shí),值會(huì)自動(dòng)按位拷貝(淺拷貝)。
#[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;
// 變量命名前加_代表這個(gè)變量處于 todo 狀態(tài),編譯器會(huì)忽視 unused 檢查
let _y = x;
// `y` is a copy of `x`
println!("{:?}", x); // A-OK!
struct:可以視為 es6 中的 class,但建議還是將 struct 視作是純粹的數(shù)據(jù)。
trait:類(lèi)似于接口,特性與接口相同的地方在于它們都是一種行為規(guī)范,可以用于標(biāo)識(shí)哪些類(lèi)有哪些方法。
derive:派生,編譯器可以通過(guò) derive 為 trait 加上一些基本實(shí)現(xiàn),如
Copy[4]:使類(lèi)型具有 “復(fù)制語(yǔ)義”而非 “移動(dòng)語(yǔ)義”。
Clone[5]:可以明確地創(chuàng)建一個(gè)值的深拷貝,在使用 Copy 的派生時(shí)一般需要把 Clone 加上,因?yàn)?Clone 是 Copy 的超集。
Debug[6]:使用
{:?}可以完整地打印當(dāng)前值。
回到 v 參數(shù)的那個(gè)問(wèn)題,因?yàn)?v 是 u32 類(lèi)型實(shí)現(xiàn)了 Copy trait,且分配在棧上,調(diào)用 find_pos 時(shí)便會(huì)自動(dòng) Copy 了一份 v' 。
但如果我們不想使用 copy 語(yǔ)義,避免內(nèi)存過(guò)多的被復(fù)制,我們可以使用“借用”數(shù)據(jù)。
值的借用
我們來(lái)看新的一個(gè)例子:
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 {
// 創(chuàng)建一個(gè)迭代器,fold 方法用法類(lèi)似 reduce
data.iter().fold(0, |acc, x| acc + x)
}
很明顯上述代碼無(wú)法通過(guò)編譯,data 和 data1 在執(zhí)行賦值語(yǔ)句和執(zhí)行 sum 方法的時(shí)候,所有權(quán)均被 move 過(guò)去了,在后面再調(diào)用他們自然會(huì)報(bào)錯(cuò)。
編譯器也非常智能地提示了我們錯(cuò)誤所在:
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 的情況下通過(guò)編譯:
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)
}
使用 & 可以來(lái)實(shí)現(xiàn) Borrow 語(yǔ)義。顧名思義,Borrow 語(yǔ)義允許一個(gè)值的所有權(quán),在不發(fā)生轉(zhuǎn)移的情況下,被其它上下文使用。
在 Rust 中,“借用”和“引用”是一個(gè)概念,同時(shí)在 Rust 下,所有的引用都只是借用了“臨時(shí)使用權(quán)”,它并不破壞值的單一所有權(quán)約束。
所以,默認(rèn)情況下,Rust 的“借用”都是只讀的。

當(dāng)然,我們對(duì)值的借用也得有一個(gè)限制:借用不能超過(guò)值的生命周期。
生命周期我們熟悉,寫(xiě) React 或者 Vue 的時(shí)候,每個(gè)組件都有從創(chuàng)建到銷(xiāo)毀的生命周期,那在 Rust 里,值的生命周期是怎么樣的呢,值的借用限制什么和生命周期有關(guān)呢,我們接著往下看。
生命周期:我們創(chuàng)建的值可以活多久
在任何語(yǔ)言里,棧上的值都有自己的生命周期,它和幀的生命周期一致。
在 Rust 中,堆上的內(nèi)存也引入生命周期的概念:除非顯式地做 Box::leak() 等動(dòng)作,一般來(lái)說(shuō),堆內(nèi)存的生命周期,會(huì)默認(rèn)和其棧內(nèi)存的生命周期綁定在一起。
Box:使用 Box<T> 允許你將一個(gè)值放在堆上而不是棧上,留在棧上的則是指向堆數(shù)據(jù)的指針。除了數(shù)據(jù)被儲(chǔ)存在堆上而不是棧上之外,box 沒(méi)有性能損失,它們多用于如下場(chǎng)景:
當(dāng)有一個(gè)在編譯時(shí)未知大小的類(lèi)型,而又想要在需要確切大小的上下文中使用這個(gè)類(lèi)型值的時(shí)候(比如遞歸類(lèi)型)
當(dāng)有大量數(shù)據(jù)并希望在確保數(shù)據(jù)不被拷貝的情況下轉(zhuǎn)移所有權(quán)的時(shí)候
我們先來(lái)看一個(gè)例子:
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("r: {}", r);
}
這段代碼并不會(huì)通過(guò)編譯,因?yàn)?r 所引用的值已經(jīng)在使用之前被釋放。
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 編譯器有一個(gè)借用檢查器,它比較作用域來(lái)確保所有的借用都是有效的,比如上述例子,我們加上生命周期的注釋再看一下:
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
} // ---------+
如你所見(jiàn),內(nèi)部的 'b 塊要比外部的生命周期 'a 小得多。在編譯時(shí),Rust 比較這兩個(gè)生命周期的大小,并發(fā)現(xiàn) r 擁有生命周期 'a,不過(guò)它引用了一個(gè)擁有生命周期 'b 的對(duì)象。程序被拒絕編譯,因?yàn)樯芷?'b 比生命周期 'a 要小:被引用的對(duì)象比它的引用者存在的時(shí)間更短。
由此,我們也解釋了 Rust 在值的借用中的那個(gè)規(guī)則:借用不能超過(guò)值的生命周期。
在 Rust 中,值的生命周期可分為:
靜態(tài)生命周期:如果一個(gè)值的生命周期貫穿整個(gè)進(jìn)程的生命周期,那么我們就稱(chēng)這種生命周期為靜態(tài)生命周期。
動(dòng)態(tài)生命周期:如果一個(gè)值是在某個(gè)作用域中定義的,也就是說(shuō)它被創(chuàng)建在棧上或者堆上,那么其生命周期是動(dòng)態(tài)的。

分配在堆和棧上的內(nèi)存有其各自的作用域,生命周期是動(dòng)態(tài)的。
全局變量、靜態(tài)變量、字符串字面量、代碼等內(nèi)容,在編譯時(shí),會(huì)被編譯到可執(zhí)行文件中,加載入內(nèi)存。生命周期和進(jìn)程的生命周期一致,生命周期是靜態(tài)的。
函數(shù)指針的生命周期也是靜態(tài)的,因?yàn)楹瘮?shù)在 Text 段中,只要進(jìn)程活著,其內(nèi)存一直存在。
有了這些概念,我們?cè)賮?lái)看一個(gè)例子:
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
}
}
同樣,這段代碼也無(wú)法通過(guò)編譯,編譯器報(bào)錯(cuò)信息如下:
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 的編譯器始終是一個(gè)良師益友,十分嚴(yán)格且優(yōu)秀,引導(dǎo)我寫(xiě)出更可靠而高效的代碼。
missing lifetime specifier 意思是編譯器在編譯 max() 函數(shù)時(shí),無(wú)法判斷 s1、s2 和返回值的生命周期。
編譯器也給了我們解決方法:手動(dòng)添加生命周期注釋?zhuān)瑏?lái)告訴編譯器 s1 和 s2 的生命周期。
fn max<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1 > s2 {
s1
} else {
s2
}
}
這個(gè)例子或許大家看起來(lái)會(huì)很疑惑,s1 和 s2 的生命周期明明一致,為什么編譯器會(huì)無(wú)法判斷他們的生命周期呢?
其實(shí)很簡(jiǎn)單,剛剛我們提到過(guò),字符串字面量的生命周期是靜態(tài)的,而 s1 是動(dòng)態(tài)的,它們的生命周期是不一致的。
當(dāng)出現(xiàn)多個(gè)參數(shù)的時(shí)候,它們的生命周期不一致,返回的值的生命周期自然也不好確定,所以這個(gè)時(shí)候,我們需要進(jìn)行生命周期標(biāo)注,告訴編譯器這些引用間生命周期的約束。
Rust 與 Webassembly
WebAssembly(wasm)可以在現(xiàn)代的網(wǎng)絡(luò)瀏覽器中運(yùn)行——它是一種低級(jí)的類(lèi)匯編語(yǔ)言,具有緊湊的二進(jìn)制格式,可以接近原生的性能運(yùn)行。
簡(jiǎn)而言之,對(duì)于網(wǎng)絡(luò)平臺(tái)而言,WebAssembly 它提供了一條途徑,以使得以各種語(yǔ)言編寫(xiě)的代碼都可以以接近原生的速度在 Web 中運(yùn)行。
對(duì)于前端而言,wasm 技術(shù)幫助解決一些場(chǎng)景下的性能瓶頸。
比如在瀏覽器中:
運(yùn)行 VR、圖像視頻編輯、3D 游戲 可以更好的讓一些語(yǔ)言和工具(如 AutoCAD)可以編譯到 Web 平臺(tái) 語(yǔ)言編譯器或虛擬機(jī)等
脫離瀏覽器的情況下:
游戲分發(fā)服務(wù)(便攜、安全)
服務(wù)端執(zhí)行不可信任的代碼。
服務(wù)端應(yīng)用
移動(dòng)混合原生應(yīng)用
接下來(lái),我們從一個(gè)圖片處理的例子入手,看一下如何從零構(gòu)建一個(gè) web-wasm 應(yīng)用。
環(huán)境準(zhǔn)備
Rust[7]
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
在安裝 Rustup 時(shí),也會(huì)安裝 Rust 構(gòu)建工具和包管理器的最新穩(wěn)定版,即 Cargo。
Cargo 可以做很多事情,如:
cargo build可以構(gòu)建項(xiàng)目
cargo run可以運(yùn)行項(xiàng)目cargo test可以測(cè)試項(xiàng)目cargo doc可以為項(xiàng)目構(gòu)建文檔cargo publish可以將庫(kù)發(fā)布到 crates.io[8]。
很明顯,Cargo 在 Rust 中扮演的 Npm 在 Node 中的角色。
Wasm-pack[9]
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
wasm-pack 用于構(gòu)建和使用我們希望與 JavaScript,瀏覽器或 Node.js 互操作的 Rust 生成的 WebAssembly。
Vite[10]
這里的前端構(gòu)建工具我使用的是 Vite,在 Vite 中還需要增加一個(gè)插件:
vite-plugin-rsw[11]:集成了 wasm-pack 的 CLI
支持 rust 包文件熱更新,監(jiān)聽(tīng) src目錄和Cargo.toml文件變更,自動(dòng)構(gòu)建
vite 啟動(dòng)優(yōu)化,如果之前構(gòu)建過(guò),再次啟動(dòng) npm run dev,則會(huì)跳過(guò)wasm-pack構(gòu)建
快速開(kāi)始
創(chuàng)建一個(gè) vite 項(xiàng)目
yarn create vite vite-webassembly
添加 vite-plugin-rsw 插件
yarn add vite-plugin-rsw -D
并增加相應(yīng)配置:
// 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`都配置過(guò)
// 會(huì)執(zhí)行,先卸載(npm unlink),再安裝(npm link)
// 例如下面會(huì)執(zhí)行
// `npm unlink picture-wasm`
unLinks: ['picture-wasm'],
// 項(xiàng)目根路徑下的rust項(xiàng)目
// `@`開(kāi)頭的為npm組織
// 例如下面會(huì)執(zhí)行:
// `npm link picture-wasm`
// 因?yàn)閳?zhí)行順序原因,雖然上面的unLinks會(huì)把`picture-wasm`卸載
// 但是這里會(huì)重新進(jìn)行安裝
crates: [ picture-wasm ],
}),
],
})
使用 cargo 初始化一個(gè) rust 項(xiàng)目
在當(dāng)前目錄下執(zhí)行:
cargo new picture-wasm
我們先來(lái)看一下現(xiàn)在的目錄結(jié)構(gòu)
[my-wasm-app] # 項(xiàng)目根路徑
|- [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] # 項(xiàng)目依賴(lài),類(lèi)似于npm的 `node_modules`
| | # 了解更多: https://doc.rust-lang.org/cargo/reference/manifest.html
| |- Cargo.toml # rust包管理清單
| - ...
|- [node_modules] # 前端的項(xiàng)目包依賴(lài)
|- [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 項(xiàng)目中,有一個(gè) Cargo.toml 文件,它就類(lèi)似于我們的 package.json ,是用作 Rust 的包管理的一個(gè)清單。
往 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:依賴(lài)列表
package:對(duì)于包的定義lib:我們當(dāng)前是屬于庫(kù)工程(src 下是 lib.rs )而不是可執(zhí)行工程(src 下是 main.rs),需要對(duì)其進(jìn)行額外設(shè)置。rlib:Rust Library 特定靜態(tài)中間庫(kù)格式。如果只是純 Rust 代碼項(xiàng)目之間的依賴(lài)和調(diào)用,那么,用 rlib 就能完全滿足使用需求(默認(rèn))。 cdylib:c 規(guī)范的動(dòng)態(tài)庫(kù),它可以公開(kāi)了 FFI 的一些功能,并可以被其他語(yǔ)言所調(diào)用。
添加 Rust 代碼
// picture-wasm/src/lib.rs
// 鏈接到 `image` 和 `base64` 庫(kù),導(dǎo)入其中的項(xiàng)
extern crate image;
extern crate base64;
// 使用 `use` 從 image 的命名空間導(dǎo)入對(duì)應(yīng)的方法
use image::DynamicImage;
use image::ImageFormat;
// 從 std(基礎(chǔ)庫(kù))的命名空間導(dǎo)入對(duì)應(yīng)方法,可用解構(gòu)的方式
use std::io::{Cursor, Read, Seek, SeekFrom};
use std::panic;
use base64::{encode};
// 引入 wasm_bindgen 下 prelude 所有模塊,用作 在 Rust 與 JavaScript 之間通信
use wasm_bindgen::prelude::*;
// 當(dāng)`wee_alloc`特性啟用的時(shí)候,使用`wee_alloc`作為全局分配器。
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
// #[wasm_bindgen] 屬性表明它下面的函數(shù)可以在JavaScript和Rust中訪問(wèn)。
#[wasm_bindgen]
extern "C" {
// 該 extern 塊將外部 JavaScript 函數(shù) console.log 導(dǎo)入 Rust。
// 通過(guò)以這種方式聲明它,wasm-bindgen 將創(chuàng)建 JavaScript 存根 console
// 允許我們?cè)?Rust 和 JavaScript 之間來(lái)回傳遞字符串。
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
fn load_image_from_array(_array: &[u8]) -> DynamicImage {
// 使用 match 進(jìn)行兜底報(bào)錯(cuò)匹配
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 聲明可變變量,類(lèi)似 js 中的 let,不使用 mut 為不可變
// 使用 Cursor 創(chuàng)建一個(gè)內(nèi)存緩存區(qū),里面是動(dòng)態(tài)數(shù)組類(lèi)型
let mut c = Cursor::new(Vec::new());
// 寫(xiě)入圖片
match _img.write_to(&mut c, ImageFormat::Png) {
Ok(c) => c,
Err(error) => {
panic!(
"There was a problem writing the resulting buffer: {:?}",
error
)
}
};
// 尋找以字節(jié)為單位的偏移量,直接用 unwrap 隱式處理 Option 類(lèi)型,直接返回值或者報(bào)錯(cuò)
c.seek(SeekFrom::Start(0)).unwrap();
// 聲明一個(gè)可變的動(dòng)態(tài)數(shù)組作輸出
let mut out = Vec::new();
c.read_to_end(&mut out).unwrap();
// 使用 encode 轉(zhuǎn)換
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` 來(lái)獲取 window 對(duì)象
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");
// 創(chuàng)建 img 元素
// 使用 `?` 在出現(xiàn)錯(cuò)誤的時(shí)候會(huì)直接返回 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 項(xiàng)目中調(diào)用 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初始化,在調(diào)用`picture-wasm`包方法時(shí)
// 必須先保證已經(jīng)進(jìn)行過(guò)初始化,否則會(huì)報(bào)錯(cuò)
// 如果存在多個(gè)wasm包,則必須對(duì)每一個(gè)wasm包進(jìn)行初始化
init();
}, []);
const fileImport = (e: any) => {
const selectedFile = e.target.files[0];
//獲取讀取我文件的File對(duì)象
// var selectedFile = document.getElementById( files ).files[0];
var reader = new FileReader(); //這是核心,讀取操作就是由它完成.
reader.readAsArrayBuffer(selectedFile); //讀取文件的內(nèi)容,也可以讀取文件的URL
reader.onload = (res: any) => {
var uint8Array = new Uint8Array(res.target.result as ArrayBuffer);
grayscale(uint8Array);
};
};
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>Hello WebAssembly!</p>
<p>Vite + Rust + React</p>
<input type="file" id="files" onChange={fileImport} />
</header>
</div>
);
}
export default App;
執(zhí)行!
在根目錄下執(zhí)行 yarn dev ,rsw 插件會(huì)打包 rust 項(xiàng)目并軟鏈接過(guò)來(lái),這樣一個(gè)本地彩色圖片轉(zhuǎn)換為黑白圖片的 web-wasm 應(yīng)用就完成了。
參考資料
[1] Rust Playground: https://play.rust-lang.org/
[2] 標(biāo)記-清除算法: 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 入門(mén)第一課 https://rust-book.junmajinlong.com/ch1/00.html
2021年 Rust 行業(yè)調(diào)研報(bào)告-InfoQ https://www.infoq.cn/article/umqbighceoa81yij7uyg 24 days from node.js to Rust
通過(guò)例子學(xué) Rust https://rustwiki.org/zh-CN/rust-by-example/hello.html
客戶(hù)端視角認(rèn)識(shí)與感受 Rust 的紅與黑 https://tech.bytedance.net/articles/7036575152028516365
實(shí)現(xiàn)一個(gè)簡(jiǎn)單的基于 WebAssembly 的圖片處理應(yīng)用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)
