作者丨lochsh 譯者丨馬可薇 策劃丨王文婧 在詳細(xì)介紹 Rust 之前,我們先舉一個(gè)例子。想象你是一個(gè)為新房子搭建煤氣管道的工人,你的老板想要你去地下室把煤氣管連到街上的主煤氣管道里,然而你下樓時(shí)卻發(fā)現(xiàn)有個(gè)小問(wèn)題,這個(gè)房子并沒(méi)有地下室。所以,現(xiàn)在你要做什么呢?什么都不做,還是異想天開地妄圖通過(guò)把煤氣主管道連到隔壁辦公室的空調(diào)進(jìn)氣口來(lái)解決問(wèn)題?不管怎么說(shuō),當(dāng)你向老板匯報(bào)任務(wù)完成時(shí),你或許會(huì)在煤氣爆炸的土灰中以刑事疏忽罪起訴。 這就是在某些編程語(yǔ)言中會(huì)發(fā)生的事。在 C 里是數(shù)組,C++ 里可能是向量,當(dāng)程序試圖尋找第 -1 個(gè)元素時(shí),什么都有可能發(fā)生:或許是每次搜索的結(jié)果都不同,讓你意識(shí)不到這里存在問(wèn)題。這種被稱作是未定義的行為,它發(fā)生的可能性并不能完全被杜絕,因?yàn)榈讓拥挠布僮鲝谋举|(zhì)上來(lái)說(shuō)并不安全,這些操作在其他的編程語(yǔ)言里可能會(huì)被編譯器警告,但是 C/C++ 并不會(huì)。 在無(wú)法保證內(nèi)存安全的情況下,未定義行為極有可能發(fā)生。漏洞 HeartBleed,一個(gè)著名的 SSL 安全漏洞,就是因?yàn)槿鄙賰?nèi)存安全防護(hù);Stagefright,同樣出名的安卓漏洞,是因?yàn)?C++ 里整數(shù)溢出造成的未定義行為。 內(nèi)存安全不止用來(lái)提防漏洞,它對(duì)應(yīng)用程序的正確運(yùn)行和可靠性同樣至關(guān)重要。可靠性的重要性在于它可以保證程序不會(huì)突然崩潰。至于準(zhǔn)確性,作者有一個(gè)曾經(jīng)在火箭飛行模擬軟件公司工作的朋友,他們發(fā)現(xiàn)傳遞相同的初始化數(shù)據(jù),但是使用不同的文件名會(huì)導(dǎo)致不同的結(jié)果,這是因?yàn)橛行┪闯跏蓟膬?nèi)存被讀取,因此模擬器就不同文件名的原因而使用了垃圾數(shù)值做基礎(chǔ),可以說(shuō)他們的這個(gè)項(xiàng)目毫無(wú)用處。 Python 和 Java 使用自動(dòng)垃圾回收來(lái)避免內(nèi)存錯(cuò)誤,例如:
自動(dòng)垃圾收集會(huì)作為 JVM 或者 Python 解釋器的一部分運(yùn)行,在程序運(yùn)行時(shí)不斷地尋找不再使用的模塊,釋放他們相對(duì)應(yīng)的內(nèi)存或者資源。但是這么做的代價(jià)很大,垃圾回收不僅速度緩慢還會(huì)占用大量?jī)?nèi)存,而你也永遠(yuǎn)不會(huì)知道下一秒你的程序會(huì)不會(huì)暫停運(yùn)行來(lái)回收垃圾。 Python 和 Java 的內(nèi)存安全犧牲了運(yùn)行速度。C/C++ 的運(yùn)行速度則是犧牲了內(nèi)存的安全性。 這種讓人無(wú)法掌控的垃圾回收讓 Python 與 Java 無(wú)法應(yīng)用在實(shí)時(shí)軟件中,因?yàn)槟惚仨氁WC你的程序可以在一定時(shí)間內(nèi)完成運(yùn)行。這并不是比拼運(yùn)行速度,而是保障你的軟件在每次運(yùn)行的時(shí)候都可以足夠迅速。 當(dāng)然,C/C++ 如此受歡迎還有其他方面的因素:他們已經(jīng)存在了足夠長(zhǎng)的時(shí)間來(lái)讓人們習(xí)慣他們了。但是他們同樣因?yàn)檫\(yùn)行速度與運(yùn)行結(jié)果的保障而受到追捧。然而不幸的是,這樣的速度是在犧牲內(nèi)存安全的前提下。更糟糕的是,許多實(shí)時(shí)軟件在保障速度的基礎(chǔ)上同樣需要注重安全性,例如車輛或者醫(yī)用機(jī)器人中的控制軟件。而這些軟件用的仍然是這些并不安全的語(yǔ)言。 在很長(zhǎng)的一段時(shí)間里,二者處于魚與熊掌不可兼得的狀態(tài),要么選擇運(yùn)行速度和不可預(yù)知性,要么選擇內(nèi)存安全和可預(yù)知性。Rust 則完全顛覆了這一點(diǎn),這也是它為什么令人激動(dòng)的原因。
Rust 的內(nèi)存安全保障說(shuō)簡(jiǎn)單也很簡(jiǎn)單,說(shuō)復(fù)雜也是復(fù)雜。簡(jiǎn)單是因?yàn)檫@里只包含了幾個(gè)非常容易理解的規(guī)則。 在 Rust 中,每一個(gè)對(duì)象有且只有一個(gè)所有者(owner),確保任何資源只能有一個(gè)綁定。為了避免被限制,在嚴(yán)格的規(guī)則下我們可以使用引用。引用在 Rsut 中經(jīng)常被稱作“借用(borrowing)”。 借用規(guī)則如下:
第一個(gè)規(guī)則避免了釋放重引用的發(fā)生,第二個(gè)規(guī)則排除了數(shù)據(jù)互斥的可能性。數(shù)據(jù)互斥會(huì)讓內(nèi)存處于未知狀態(tài),而它可由這三個(gè)行為造成:
當(dāng)作者還是嵌入式工程師的時(shí)候,堆(heap)還沒(méi)有出現(xiàn),于是便在硬件上設(shè)置了一個(gè)空指針解引用的陷阱,這樣一來(lái),很多常見(jiàn)的內(nèi)存問(wèn)題就顯得不是那么重要了。數(shù)據(jù)互斥是作者當(dāng)時(shí)最怕的一種 bug;它難以追蹤,當(dāng)你修改了一部分看起來(lái)并不重要的代碼,或是外部條件發(fā)生了微小的改變時(shí),互斥的勝利者也就易位了。Therac-25 事件,就是因?yàn)閿?shù)據(jù)互斥使得癌癥病人在治療過(guò)程中受到了過(guò)量的輻射,因此造成患者死亡或者重傷。 Rust 革新的關(guān)鍵也是它聰明的地方,它可以在編譯時(shí)強(qiáng)制執(zhí)行內(nèi)存安全保障。這些規(guī)則對(duì)任何接觸過(guò)數(shù)據(jù)互斥的人來(lái)說(shuō)都應(yīng)當(dāng)不是什么新鮮事。 如作者之前所說(shuō),未定義行為發(fā)生的可能性是不能完全被清除的,這是由于底層計(jì)算機(jī)硬件固有的不安全性導(dǎo)致的。Rust 允許在一個(gè)存放不安全代碼的模塊進(jìn)行不安全操作。C# 和 Ada 應(yīng)該也有類似禁用安全檢查的方案。在進(jìn)行嵌入式編程操作或者在底層系統(tǒng)編程的時(shí)候,就會(huì)需要這樣的一個(gè)塊。隔離代碼的潛在不安全部分非常有用,這樣一來(lái),與內(nèi)存相關(guān)的錯(cuò)誤就必定位于這個(gè)模塊內(nèi),而不是整個(gè)程序的任意部分。 不安全模塊并不會(huì)關(guān)閉借用檢查,用戶可以在不安全塊中進(jìn)行解引用裸引針,訪問(wèn)或修改可變靜態(tài)變量,所有權(quán)系統(tǒng)的優(yōu)點(diǎn)仍然存在。 說(shuō)起所有權(quán),就不得不提起 C++ 的所有權(quán)機(jī)制。 C++ 中的所有權(quán)在 C++11 發(fā)布之后得到了極大的提升,但是它也為向后兼容性問(wèn)題付出了不小的代價(jià)。對(duì)于作者來(lái)說(shuō),C++ 的所有權(quán)非常多余,以前簡(jiǎn)單的值分類被吊打。不管怎么說(shuō),對(duì) C++ 這樣廣泛使用的語(yǔ)言進(jìn)行大規(guī)模優(yōu)化是一項(xiàng)偉大的成就,但是 Rust 卻是將所有權(quán)從一開始就當(dāng)作核心理念進(jìn)行設(shè)計(jì)的語(yǔ)言。 C++ 的類型系統(tǒng)不會(huì)對(duì)對(duì)象模型的生命周期進(jìn)行建模,因此在運(yùn)行時(shí)是無(wú)法檢查釋放后重引用的問(wèn)題。C++ 的智能指針只是加在舊系統(tǒng)上的一個(gè)庫(kù),而這個(gè)庫(kù)會(huì)以 Rust 中不被允許的方式濫用和誤用。 下面是作者在工作中編寫的一些經(jīng)過(guò)簡(jiǎn)化后的代碼,代碼中存在誤用的問(wèn)題。 std::vector<std::string> dataCheckStrs) { autocreateCheck = & { returnDataValueCheck(checkStr,std::move(data)); std::back_inserter(checks), createCheck); returnchecks; } 這段代碼的作用是,通過(guò)字符串 dataCheckStrs 定義對(duì)某些數(shù)據(jù)的檢查,例如一個(gè)特定范圍內(nèi)的值,然后再通過(guò)解析這個(gè)字符串創(chuàng)建一個(gè)用于檢查對(duì)象的向量。 首先創(chuàng)建一個(gè)引用捕捉的 lambda 表達(dá)式,由 & 標(biāo)識(shí),這個(gè)智能指針(unique_ptr)指向的對(duì)象在這個(gè) lambda 內(nèi)被移動(dòng),因此是非法的。 然后用被移動(dòng)的數(shù)據(jù)構(gòu)建的檢查填充向量,但問(wèn)題是它只能完成第一步。unique_ptr 和被指向?qū)ο蟊硎疽环N獨(dú)自占有的關(guān)系,不能被拷貝。所以在 std::transform 的第一個(gè)循環(huán)之后,unique_ptr 很有可能被清空,官方聲明是它會(huì)處于一種有效但是未知的狀態(tài),但是以作者對(duì) Clang 的經(jīng)驗(yàn)來(lái)看它通常會(huì)被清空。 后續(xù)使用這個(gè)空指針時(shí)會(huì)導(dǎo)致未定義行為,作者運(yùn)行之后得到了一個(gè)空指針錯(cuò)誤,在大多數(shù)托管系統(tǒng)的空指針解引用都會(huì)報(bào)這種錯(cuò)誤,因?yàn)榱銉?nèi)存頁(yè)面通常會(huì)被保留。但當(dāng)然這種情況并不會(huì)百分百發(fā)生,這種 bug 在理論上可能會(huì)被暫時(shí)擱置一段時(shí)間,然后等著你的就是程序的突然崩潰。 這里使用 lambda 的方式很大程度上導(dǎo)致了這種危險(xiǎn)的發(fā)生。編譯器在調(diào)用時(shí)只能看到以一個(gè)函數(shù)指針,它并不能像標(biāo)準(zhǔn)函數(shù)那樣檢查 lambda。 結(jié)合上下文來(lái)理解這個(gè) bug 的話,最初使用 shared_ptr 來(lái)存儲(chǔ)數(shù)據(jù),這一部分沒(méi)有問(wèn)題。然而我們卻錯(cuò)誤地將數(shù)據(jù)存儲(chǔ)在了 unique_ptr 里,當(dāng)我們?cè)噲D進(jìn)行更改時(shí)就會(huì)有問(wèn)題,它并沒(méi)有引起注意是因?yàn)榫幾g器并沒(méi)有報(bào)錯(cuò)。 這是 C++ 內(nèi)存安全問(wèn)題并沒(méi)有引起重視的真實(shí)例子,作者和審核代碼的人直到一次測(cè)試前都沒(méi)有注意到這點(diǎn)。不管你有多少年的編程經(jīng)驗(yàn),這類 bug 根本躲不開!哪怕是編譯器都不能拯救你。這時(shí)就需要更好的工具了,不僅僅是為了我們的理智著想,也是為了公眾安全,這關(guān)乎職業(yè)道德。 接下來(lái)讓我們看一看同樣問(wèn)題在 Rust 中的體現(xiàn)。 在 Rust 中,這種糟糕的 move 是不會(huì)被允許的。 letcreate_check = |check_str: &String| DataValueCheck::new(check_str, data); 這是我們第一次看到 Rust 的代碼。需要注意的是,默認(rèn)情況下變量都是不可變的,但可以在變量前加 mut 關(guān)鍵詞使其可變,mut 類似于 C/C++ 中的 const 的反義詞。 Box 類型則表示我們已經(jīng)在堆上分配了內(nèi)存,在這里使用是因?yàn)?unique_ptr 同樣可以分配到堆。因?yàn)?Rust 中每個(gè)對(duì)象一次有且僅有一個(gè)所有者的規(guī)則,我們并不需要任何 unique_ptr 類似的東西。接著創(chuàng)建一個(gè)閉包,用更高階的函數(shù) map 轉(zhuǎn)換字符串,類似 C++ 的方式,但并不顯得冗長(zhǎng)。但當(dāng)編譯的時(shí)候還是會(huì)報(bào)錯(cuò),下面是錯(cuò)誤信息: 6| letcreate_check = |check_str: &String| DataValueCheck::new(check_str, data); | | || | | closure is`FnOnce`because it moves | | the variable`data`out of its environment | thisclosureimplements`FnOnce`, not`FnMut` 7| data_check_strs.iter.map(create_check).collect | --- the requirement to implement`FnMut`derivesfromhere error: aborting due to previous error For more information aboutthiserror,try`rustc --explain E0525`. Rust 社區(qū)有一點(diǎn)很棒,它提供給人們的學(xué)習(xí)資源非常多,也會(huì)提供可讀性的錯(cuò)誤信息,用戶甚至可以向編譯器詢問(wèn)關(guān)于錯(cuò)誤的更詳細(xì)信息,而編譯器則會(huì)回復(fù)一個(gè)帶有解釋的最小示例。 當(dāng)創(chuàng)建閉包時(shí),由于有且僅有一個(gè)所有者的規(guī)則,數(shù)據(jù)是在其內(nèi)被移動(dòng)的。接下來(lái)編譯器推斷閉包只能運(yùn)行一次:沒(méi)有所有權(quán)的原因,多次的運(yùn)行是非法的。之后 map 函數(shù)就會(huì)需求一個(gè)可以重復(fù)調(diào)用并且處于可變狀態(tài)的可調(diào)用函數(shù),這就是為什么編譯器會(huì)失敗的原因。 這一段代碼顯示了 Rust 中類型系統(tǒng)與 C++ 相比有多么強(qiáng)大,同時(shí)也體現(xiàn)了在當(dāng)編譯器跟蹤對(duì)象生命周期時(shí)的語(yǔ)言中編程是多么不同。 在示例中的錯(cuò)誤信息里提到了特質(zhì)(trait)。例如:”缺少實(shí)現(xiàn) FnMut 特質(zhì)的閉包“。特質(zhì)是一種告訴 Rust 編譯器某個(gè)特定類型擁有功能的語(yǔ)言特性,特質(zhì)也是 Rust 多態(tài)機(jī)制的體現(xiàn)。 C++ 支持多種形式的多態(tài),作者認(rèn)為這有助于語(yǔ)言的豐富性。靜態(tài)多態(tài)中有模板、函數(shù)和以及操作符重載;動(dòng)態(tài)多態(tài)有子類。但這些表達(dá)形式也有非常明顯的缺點(diǎn):子類與父類之間的緊密耦合,導(dǎo)致子類過(guò)于依賴父類,缺乏獨(dú)立性;模板則因?yàn)槠淙狈?shù)化的特性而導(dǎo)致調(diào)試?yán)щy。 Rust 中的 trait 則定義了一種指定靜態(tài)動(dòng)態(tài)接口共享的行為。Trait 類似于其他語(yǔ)言中接口(interface)的功能,但 Rust 中只支持實(shí)現(xiàn)(implements)而沒(méi)有繼承(extends)關(guān)系,鼓勵(lì)基于組合的設(shè)計(jì)而不是實(shí)現(xiàn)繼承,降低耦合度。 下面來(lái)看一個(gè)簡(jiǎn)單又有趣的例子: traitRateable{ /// Rate fluff out of 10 /// Ratings above 10 for exceptionally soft bois fn fluff_rating(&self) -> f32; } days_since_shearing: f32, age: f32 } fn fluff_rating(&self) -> f32 { 10.0*365.0/self.days_since_shearing } } 首先定義一個(gè)名為 Rateable 的 trait,然后需要調(diào)用函數(shù) fluff_rating 并返回一個(gè)浮點(diǎn)數(shù)來(lái)實(shí)現(xiàn) Rateable。接著就是在 Alpaca 結(jié)構(gòu)體上對(duì) Rateable trait 的實(shí)現(xiàn)。下面是使用同樣的方法定義 Cat 類型。 enum Coat { Hairless, Short, Medium, Long } struct Cat { coat: Coat, age: f32 } impl RateableforCat { fn fluff_rating(&self) -> f32 { matchself.coat { Coat::Hairless =>0.0, Coat::Short =>5.0, Coat::Medium =>7.5, Coat::Long =>10.0 在這段例子中作者使用了 Rust 的另一特性,模式匹配。它與 C 中的 switch 語(yǔ)句用法類似,但在語(yǔ)義上卻有很大的區(qū)別。switch 塊中的 case 只能用來(lái)跳轉(zhuǎn),模式匹配中則要求覆蓋全部可能性才能編譯成功,但可選的匹配范圍和結(jié)構(gòu)則賦予了其靈活性。 下面是這兩種類型的實(shí)現(xiàn)結(jié)合得出的通用函數(shù): fn pet<T: Rateable>(boi: T) -> &str { match boi.fluff_rating { 0.0...3.5=>'naked alien boi...but precious nonetheless', 3.5...6.5=>'increased floof...increased joy', 6.5...8.5=>'approaching maximum fluff', _ =>'sublime. the softest boi!' } 尖括號(hào)中的是類型參數(shù),這一點(diǎn)和 C++ 中相同,但與 C++ 模板的不同之處在于我們可以使函數(shù)參數(shù)化?!按撕瘮?shù)只適用于 Rateable 類型”的說(shuō)法在 Rust 中是可以的,但在 C++ 中卻毫無(wú)意義,這帶來(lái)的后果不僅限于可讀性。類型參數(shù)上的 trait bound 意味著 Rust 的編譯器可以只對(duì)函數(shù)進(jìn)行一次類型檢查,避免了單獨(dú)檢查每個(gè)具體的實(shí)現(xiàn),從而縮短編譯時(shí)間并簡(jiǎn)化了編譯錯(cuò)誤信息。 Trait 也可以動(dòng)態(tài)使用,雖然有的時(shí)候是必須的,但是并不推薦,因?yàn)闀?huì)增加運(yùn)行開銷,所以作者在本文中并沒(méi)有詳細(xì)提及。Trait 中另一大部分就是它的互通性,例如標(biāo)準(zhǔn)庫(kù)中的 Display 和 Add trait。實(shí)現(xiàn) add trait 意味著可以重載運(yùn)算符 +,實(shí)現(xiàn) display trait 則意味著可以格式化輸出顯示。 C/C++ 中并沒(méi)有用于管理依賴的標(biāo)準(zhǔn),倒是有不少工具可以提供幫助,但是它們的口碑都不是很好。基礎(chǔ)的 Makefiles 用于構(gòu)建系統(tǒng)非常靈活,但在維護(hù)上就是一團(tuán)垃圾。CMake 減少了維護(hù)的負(fù)擔(dān),但是它的靈活性較弱,又很讓人煩惱。 Rust 在這方面就很優(yōu)秀,Cargo 是唯一 Rust 社區(qū)中唯一的可以用來(lái)管理包和依賴,同時(shí)還可以用來(lái)搭建和運(yùn)行項(xiàng)目。它的地位與 Python 中的 Pipenv 和 Poetry 類似。官方安裝包會(huì)自帶 Cargo,它好用到讓人遺憾為什么 C/C++ 中沒(méi)有類似的工具。 這個(gè)問(wèn)題沒(méi)有標(biāo)準(zhǔn)答案,完全取決于用戶的應(yīng)用程序場(chǎng)景,這一點(diǎn)在任何編程語(yǔ)言中都是共通的。Rust 在不同方面都有成功的案例:包括微軟的 Azure IoT 項(xiàng)目,Mozilla 也支持 Rust 并將用于部分火狐瀏覽器中,同樣很多人也在使用 Rust。Rust 已經(jīng)日漸成熟并可以用于生產(chǎn),但對(duì)于某些應(yīng)用程序來(lái)說(shuō),它可能還不夠成熟或缺乏支持庫(kù)。 1、嵌入式:在嵌入式的環(huán)境中,Rust 的使用體驗(yàn)完全由用戶定義用它做什么。Cortex-M 已經(jīng)資源成熟并可以用于生產(chǎn)了,RISC-V 也有了一個(gè)還在發(fā)展尚未常熟的工具鏈。. x86 和 arm8 架構(gòu)也發(fā)展得不錯(cuò),其中就有 Raspberry Pi。像是 PIC 和 AVR 這樣的老式架構(gòu)還有些欠缺,但作者認(rèn)為,對(duì)于大多數(shù)的新項(xiàng)目來(lái)說(shuō)應(yīng)該沒(méi)什么大問(wèn)題。 交叉編譯支持也適用于所有的 LLVM(Low-Level Virtual Machine)的目標(biāo),因?yàn)?Rust 使用 LLVM 作為其編譯器后端。 Rust 在嵌入式中缺少的另一個(gè)部分是生產(chǎn)級(jí)的 RTOS,在 HAL 的發(fā)展也很匱乏。對(duì)許多項(xiàng)目來(lái)說(shuō),這沒(méi)什么大不了了,但對(duì)另一些項(xiàng)目的阻礙依舊存在。在未來(lái)幾年內(nèi),阻礙可能還會(huì)繼續(xù)增加。 2、異步:語(yǔ)言的異步支持還尚在開發(fā)階段,async/await 的語(yǔ)法都還未被確定。 3、互通性:至于與其他語(yǔ)言的互操作性,Rust 有一個(gè) C 的外部函數(shù)接口(FFI),無(wú)論是 C++ 到 Rust 函數(shù)的回調(diào)還是將 Rust 對(duì)象作為回調(diào),都需要經(jīng)過(guò)這一步。在很多語(yǔ)言中這都是非常普遍的,在這里提到則是因?yàn)槿绻麑?Rust 合并到現(xiàn)有的 C++ 項(xiàng)目中會(huì)有些麻煩,因?yàn)橛脩粜枰?Rust 和 C++ 中添加一個(gè) C 語(yǔ)言層,這毫無(wú)疑問(wèn)會(huì)帶來(lái)很多問(wèn)題。 如果要在工作中從頭開始一個(gè)項(xiàng)目,那么作者絕對(duì)會(huì)選擇 Rust 編程語(yǔ)言。希望 Rust 可以成為一個(gè)更可靠,更安全,也更令人享受的未來(lái)編程語(yǔ)言。 |
|