Rust突破編譯器限制構(gòu)造可修改的全局變量
問題
在前面一些章節(jié)里,在使用正則表達(dá)式對文本進(jìn)行分割時,皆使用局部變量存儲正則表達(dá)式,且皆為硬代碼——在程序運(yùn)行時無法修改正則表達(dá)式。本章嘗試構(gòu)造一個可在運(yùn)行時被修改的全局變量用以表達(dá)正則表達(dá)式。
失敗的全局原始指針
倘若將原始指針作為全局變量,在程序運(yùn)行時,可以令其指向與其類型相匹配的任何一個值,這是我想要的全局變量。于是,試著寫出以下代碼:
use regex::Regex; use std::ptr::null_mut; let a: *mut Regex = null_mut(); fn main() { a = Box::into_raw(Box::new(Regex::new(" *@ *"))); let v = (*a).unwrap().split("num@ 123@456 @ 789"); for i in v { println!("{}", i); } }
Rust 編譯器編譯上述代碼時會報錯,建議使用 const
或 static
代替全局變量 a
的定義語句中的 let
,亦即 Rust 語言不允許使用 let
定義全局變量。const
修飾的全局變量,其值不可修改。static
修飾的全局變量,其值可修改。故而,我將變量 a
的定義修改為
static a: *mut Regex = null_mut();
Rust 編譯器依然報錯,稱 *mut regex::Regex
類型的值不能被不同的線程安全共享,雖不甚知其意,但也應(yīng)知此路不通了。
也許在素有經(jīng)驗(yàn)的 Rust 程序員看來,上述代碼會令他一言難盡,但是如果我說通過以上代碼可以看出 Rust 語言并不希望程序員使用全局變量,料想不會引起他的反對。Rust 不希望什么,那是它的事,而我卻需要它。現(xiàn)在的問題是,無法構(gòu)造全局原始指針。Rust 編譯器給出的建議是,如果想讓 *mut regex::Regex
類型的指針作為全局變量,前提是需要為該類型實(shí)現(xiàn) Sync
特性。這個建議對于目前的我來說是超綱的,所以我完全可以認(rèn)為,在 Rust 語言中不允許出現(xiàn)全局原始指針。
Option<T> 于事無補(bǔ)
在表示空值方面,Option<T>
類型可以代替原始指針,用該類型封裝原始指針是否能作為全局變量呢?試試看:
static foo: Option<*mut i32> = None; fn main() { let a = 3; foo = Some(&a as *mut i32); println!("{:?}", foo); }
答案是否定的。Rust 編譯器依然稱:
`*mut i32` cannot be shared between threads safely
并建議
shared static variables must have a type that implements `Sync`
此路依然不通。
結(jié)構(gòu)體屏障
無論是直接用原始指針,還是用 Option<T>
封裝原始指針,在構(gòu)造全局變量時,都會導(dǎo)致原始指針直接暴露在 Rust 編譯器面前,而編譯器堅(jiān)持認(rèn)為,所有的全局變量類型都應(yīng)該實(shí)現(xiàn) Sync
特性?,F(xiàn)在,換一個思路,倘若將原始指針類型封裝在結(jié)構(gòu)體中,是否可以騙過編譯器呢?
以下代碼將 *mut i32
類型的指針封裝在一個結(jié)構(gòu)體類型中,并使用該結(jié)構(gòu)體類型構(gòu)造全局變量:
#[derive(Debug)] struct Foo { data: *mut i32 } static mut A: Foo = Foo{data: std::ptr::null_mut()}; fn main() { unsafe { println!("{:?}", A); } }
上述程序可以通過編譯,其輸出為
Foo { data: 0x0 }
以下代碼嘗試能否修改 A.data
的值:
let mut a = 3; unsafe { A.data = &mut a as *mut i32; println!("{:?}", A); println!("{}", *A.data); }
依然能通過編譯,其輸出結(jié)果與以下結(jié)果類似:
Foo { data: 0x7fff64cdecb4 }
3
這樣騙編譯器,好么?我不知道。Rust 標(biāo)準(zhǔn)庫在 std::marker::Sync
的文檔中提到,所有的基本類型,復(fù)合類型(元組、結(jié)構(gòu)體和枚舉),引用,Vec<T>
,Box<T
以及大多數(shù)集合類型等皆實(shí)現(xiàn)了 Sync
特性,所以上述手法并不能稱為「騙」。
回到本章開始的問題,現(xiàn)在可寫出以下代碼:
use regex::Regex; use std::ptr::null_mut; #[derive(Debug)] struct Foo { data: *mut Regex } static mut A: Foo = Foo{data: null_mut()}; fn main() { unsafe { A = Foo {data: Box::into_raw(Box::new(Regex::new(" *@ *").unwrap()))}; let v = (*A.data).split("num@ 123@456 @ 789"); for i in v { println!("{}", i); } let _ = Box::from_raw(A.data); } }
注意,上述代碼中的 let _ = ...
表示不關(guān)心右側(cè)函數(shù)調(diào)用的返回值,但是該行代碼可將 A.data
指向的內(nèi)存空間歸還于 Rust 的智能指針管理系統(tǒng),從而實(shí)現(xiàn)自動釋放。
制造內(nèi)存泄漏
上述基于原始指針的全局變量構(gòu)造方法似乎并不為 Rust 開發(fā)者欣賞,因?yàn)樵谒麄冄劾铮魏我粋€原始指針都像一個不知道什么時候會被一腳踩上去的地雷,他們更喜歡是引用。
下面嘗試使用引用構(gòu)造全局變量。由于引用不具備空值,所以必須使用 Option<T>
進(jìn)行封裝,例如
use regex::Regex; static mut A: Option<&Regex> = None; fn main() { unsafe { let re = Regex::new(" *@ *").unwrap(); A = Some(&re); // ... 待補(bǔ)充 } }
Rust 編譯器對上述代碼給出的錯誤信息是,re
被一個全局變量借用,但是前者的壽命短于后者,亦即當(dāng)后者還存在時,前者已經(jīng)死亡,導(dǎo)致后者引用失效。在 C 語言中,這種錯誤就是鼎鼎有名的「懸垂指針」錯誤,Rust 編譯器會盡自己最大能力去阻止此類錯誤。
不過,Rust 標(biāo)準(zhǔn)庫給我們留了一個后門,使用 Box<T>
的 leak
方法可將位于堆空間的值的壽命提升為全局變量級別的壽命:
unsafe { let re = Box::new(Regex::new(" *@ *").unwrap()); A = Some(Box::leak(re)); let v = A.unwrap().split("num@ 123@456 @ 789"); for i in v { println!("{}", i); } }
需要注意的是,Box::leak
名副其實(shí),會導(dǎo)致內(nèi)存泄漏,因?yàn)槎芽臻g的值其壽命經(jīng) Box::leak
提升后,與程序本身相同,無法回收。Rust 官方說,如果你介意這樣的內(nèi)存泄漏,那就需要考慮走原始指針路線。
延遲初始化
對于支持運(yùn)行時修改的全局變量,還有一類方法是將全局變量的初始化推遲在程序運(yùn)行時,但該類方法要么依賴第三方庫(crate),例如 lazy_static,要么是標(biāo)準(zhǔn)庫目前尚未穩(wěn)定的功能 OnceCell,此外該類方法只能對全局變量完成一次賦值。這些方法,rzeo 并不打算使用,故而略過。
值的所有權(quán)轉(zhuǎn)移
基于值的所有權(quán)轉(zhuǎn)移也能實(shí)現(xiàn)在程序的運(yùn)行時修改全局變量的值。例如
use regex::Regex; static mut A: Option<Regex> = None; fn main() { unsafe { let re = Regex::new(" *@ *").unwrap(); A = Some(re); let v = A.unwrap().split("num@ 123@456 @ 789"); for i in v { println!("{}", i); } } }
不過,上述代碼無法通過編譯,原因是 Option<T>
的實(shí)例方法 unwrap
需要轉(zhuǎn)移實(shí)例的所有權(quán)——消耗一個臨時變量,但是上述代碼中的 Option<T>
的實(shí)例 A
是全局變量,與程序同壽,其所有權(quán)無法轉(zhuǎn)移。有兩種方法可規(guī)避該錯誤,一種是
unsafe { let re = Regex::new(" *@ *").unwrap(); A = Some(re); match A { Some(ref b) => { let v = b.split("num@ 123@456 @ 789"); for i in v { println!("{}", i); } }, None => panic!("...") } }
另一種是使用 Option<T>
的 as_ref
方法,將類型 &Option<T>
轉(zhuǎn)換為類型 Option<&T>
,然后使用 Option<&T>
的 unwrap
方法:
unsafe { let re = Regex::new(" *@ *").unwrap(); A = Some(re); let v = A.as_ref().unwrap().split("num@ 123@456 @ 789"); for i in v { println!("{}", i); } }
不妨將 as_ref
方法視為上述模式匹配代碼的簡化。
小結(jié)
全局變量是構(gòu)成程序的不安全因素之一,但它并非洪水猛獸,只要保證程序在任一時刻全局變量不會被多個線程同時修改即可。如果全局變量給程序帶來了災(zāi)難,這往往意味著是程序的設(shè)計(jì)出現(xiàn)了嚴(yán)重問題。我認(rèn)為 Rust 對全局變量的限制太過于嚴(yán)厲,特別是在禁止直接將原始指針作為全局變量這一方面,畢竟即使不使用原始指針,對全局變量的修改在 Rust 語言看來,也是不安全的。既然都不安全,何必五十步笑百步。
以上就是Rust突破編譯器限制構(gòu)造可修改的全局變量的詳細(xì)內(nèi)容,更多關(guān)于Rust全局變量的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Rust中的方法與關(guān)聯(lián)函數(shù)使用解讀
在Rust中,方法是定義在特定類型(如struct)的impl塊中,第一個參數(shù)是self(可變或不可變),方法用于描述該類型實(shí)例的行為,而關(guān)聯(lián)函數(shù)則不包含self參數(shù),常用于構(gòu)造新實(shí)例或提供一些與實(shí)例無關(guān)的功能,Rust的自動引用和解引用特性使得方法調(diào)用更加簡潔2025-02-02Rust 利用 chrono 庫實(shí)現(xiàn)日期和字符串互相轉(zhuǎn)換的示例
在Rust中,chrono庫提供了強(qiáng)大的日期和時間處理功能,使得日期與字符串之間的轉(zhuǎn)換變得簡單,本文介紹了如何在Rust中使用chrono庫將日期轉(zhuǎn)換成字符串,以及如何將字符串解析為日期,對于需要進(jìn)行日期時間格式化、解析或進(jìn)行時區(qū)處理的開發(fā)者來說,chrono庫是一個不可或缺的工具2024-11-11