禁止引用 static mut
概述
static_mut_refs
lint 级别现在默认为deny
。这会检查是否获取了static mut
的共享或可变引用。
详情
static_mut_refs
lint 检测到获取了 static mut
的引用。在 2024 Edition 中,此 lint 现在默认为 deny
,以强调您应避免创建这些引用。
#![allow(unused)] fn main() { static mut X: i32 = 23; static mut Y: i32 = 24; unsafe { let y = &X; // ERROR: shared reference to mutable static let ref x = X; // ERROR: shared reference to mutable static let (x, y) = (&X, &Y); // ERROR: shared reference to mutable static } }
仅仅获取这样的引用就违反了 Rust 的可变性 XOR 别名要求,这始终是即时的 未定义行为,即使该引用从未被读取或写入。此外,为了维护 static mut
的可变性 XOR 别名,需要全局性地推理您的代码,这在面对重入和/或多线程时可能尤其困难。
请注意,在某些情况下,隐式引用会自动创建,而没有可见的 &
运算符。例如,以下情况也会触发 lint
#![allow(unused)] fn main() { static mut NUMS: &[u8; 3] = &[0, 1, 2]; unsafe { println!("{NUMS:?}"); // ERROR: shared reference to mutable static let n = NUMS.len(); // ERROR: shared reference to mutable static } }
替代方案
在任何可能的情况下,强烈建议改用不可变的 static
,其类型在某些局部推理的抽象背后提供内部可变性(这大大降低了确保 Rust 的可变性 XOR 别名要求得到维护的复杂性)。
在无法进行局部推理的抽象,因此您仍然不得不全局性地推理对 static
变量的访问的情况下,您现在必须使用原始指针,例如可以通过 &raw const
或 &raw mut
运算符 获取的原始指针。通过首先获取原始指针而不是直接获取引用,通过该指针访问的(安全要求)对于 unsafe
开发者来说会更熟悉,并且可以推迟到/限制在较小的代码区域内。
请注意,以下示例仅为说明,并非旨在作为完整的实现。请勿按原样复制这些示例。您的具体情况可能需要修改某些细节以满足您的需求。这些示例旨在帮助您了解解决问题的不同方法。
建议阅读标准库中特定类型的文档,关于 未定义行为 的参考,Rustonomicon,如果您有疑问,请在 Rust 论坛(例如 Users Forum)上寻求帮助。
不要使用全局变量
这可能是您已经知道的,但如果可能,最好避免可变全局状态。当然,有时这可能会有点笨拙或困难,特别是如果您需要在许多函数之间传递可变引用。
原子类型
原子类型提供了可以在 static
(不带 mut
)中使用的整数、指针和布尔值。
use std::sync::atomic::Ordering; use std::sync::atomic::AtomicU64; // Chnage from this: // static mut COUNTER: u64 = 0; // to this: static COUNTER: AtomicU64 = AtomicU64::new(0); fn main() { // Be sure to analyze your use case to determine the correct Ordering to use. COUNTER.fetch_add(1, Ordering::Relaxed); }
互斥锁或读写锁
当您的类型比原子类型更复杂时,请考虑使用 Mutex
或 RwLock
来确保对全局值的正确访问。
use std::sync::Mutex; use std::collections::VecDeque; // Change from this: // static mut QUEUE: VecDeque<String> = VecDeque::new(); // to this: static QUEUE: Mutex<VecDeque<String>> = Mutex::new(VecDeque::new()); fn main() { QUEUE.lock().unwrap().push_back(String::from("abc")); let first = QUEUE.lock().unwrap().pop_front(); }
OnceLock 或 LazyLock
如果您因为需要执行一些不能是 const
的一次性初始化而使用 static mut
,您可以改为使用 OnceLock
或 LazyLock
。
use std::sync::LazyLock; struct GlobalState; impl GlobalState { fn new() -> GlobalState { GlobalState } fn example(&self) {} } // Instead of some temporary or uninitialized type like: // static mut STATE: Option<GlobalState> = None; // use this instead: static STATE: LazyLock<GlobalState> = LazyLock::new(|| { GlobalState::new() }); fn main() { STATE.example(); }
OnceLock
类似于 LazyLock
,但如果您需要将信息传递到构造函数中,则可以使用它,这可以很好地与单次初始化点(如 main
)一起使用,或者如果输入在您访问全局变量的任何地方都可用。
use std::sync::OnceLock; struct GlobalState; impl GlobalState { fn new(verbose: bool) -> GlobalState { GlobalState } fn example(&self) {} } struct Args { verbose: bool } fn parse_arguments() -> Args { Args { verbose: true } } static STATE: OnceLock<GlobalState> = OnceLock::new(); fn main() { let args = parse_arguments(); let state = GlobalState::new(args.verbose); let _ = STATE.set(state); // ... STATE.get().unwrap().example(); }
no_std
一次性初始化
此示例类似于 OnceLock
,因为它提供了全局变量的一次性初始化,但它不需要 std
,这在 no_std
上下文中很有用。假设您的目标支持原子操作,那么您可以使用原子操作来检查全局变量的初始化。该模式可能如下所示
use core::sync::atomic::AtomicUsize; use core::sync::atomic::Ordering; struct Args { verbose: bool, } fn parse_arguments() -> Args { Args { verbose: true } } struct GlobalState { verbose: bool, } impl GlobalState { const fn default() -> GlobalState { GlobalState { verbose: false } } fn new(verbose: bool) -> GlobalState { GlobalState { verbose } } fn example(&self) {} } const UNINITIALIZED: usize = 0; const INITIALIZING: usize = 1; const INITIALIZED: usize = 2; static STATE_INITIALIZED: AtomicUsize = AtomicUsize::new(UNINITIALIZED); static mut STATE: GlobalState = GlobalState::default(); fn set_global_state(state: GlobalState) { if STATE_INITIALIZED .compare_exchange( UNINITIALIZED, INITIALIZING, Ordering::SeqCst, Ordering::SeqCst, ) .is_ok() { // SAFETY: The reads and writes to STATE are guarded with the INITIALIZED guard. unsafe { STATE = state; } STATE_INITIALIZED.store(INITIALIZED, Ordering::SeqCst); } else { panic!("already initialized, or concurrent initialization"); } } fn get_state() -> &'static GlobalState { if STATE_INITIALIZED.load(Ordering::Acquire) != INITIALIZED { panic!("not initialized"); } else { // SAFETY: Mutable access is not possible after state has been initialized. unsafe { &*&raw const STATE } } } fn main() { let args = parse_arguments(); let state = GlobalState::new(args.verbose); set_global_state(state); // ... let state = get_state(); state.example(); }
此示例假定您可以在静态变量初始化之前在其中放入一些默认值(本示例中的 const default
构造函数)。如果这不可能,请考虑使用 MaybeUninit
、动态特征分发(使用实现特征的虚拟类型)或其他一些方法来获得默认占位符。
有一些社区提供的 crate 可以提供类似的一次性初始化,例如 static-cell
crate(它通过使用 portable-atomic
来支持没有原子操作的目标)。
原始指针
在某些情况下,您可以继续使用 static mut
,但避免创建引用。例如,如果您只需要将 原始指针 传递到 C 库中,请不要创建中间引用。相反,您可以使用 原始借用运算符,如下例所示
#[repr(C)] struct GlobalState { value: i32 } impl GlobalState { const fn new() -> GlobalState { GlobalState { value: 0 } } } static mut STATE: GlobalState = GlobalState::new(); unsafe extern "C" { fn example_ffi(state: *mut GlobalState); } fn main() { unsafe { // Change from this: // example_ffi(&mut STATE as *mut GlobalState); // to this: example_ffi(&raw mut STATE); } }
请注意,您仍然需要维护围绕可变指针的别名约束。这可能需要一些内部或外部同步,或者关于如何在线程、中断处理程序和重入之间使用它的证明。
带有 Sync
的 UnsafeCell
UnsafeCell
未实现 Sync
,因此不能在 static
中使用。您可以围绕 UnsafeCell
创建自己的包装器以添加 Sync
实现,以便它可以在 static
中用于实现内部可变性。如果您有外部锁或其他保证来维护可变指针所需的安全不变量,则此方法可能很有用。
请注意,这在很大程度上与 原始指针 示例相同。包装器有助于强调您如何使用该类型,并专注于您应注意哪些安全要求。但除此之外,它们大致相同。
#![allow(unused)] fn main() { use std::cell::UnsafeCell; fn with_interrupts_disabled<T: Fn()>(f: T) { // A real example would disable interrupts. f(); } #[repr(C)] struct GlobalState { value: i32, } impl GlobalState { const fn new() -> GlobalState { GlobalState { value: 0 } } } #[repr(transparent)] pub struct SyncUnsafeCell<T>(UnsafeCell<T>); unsafe impl<T: Sync> Sync for SyncUnsafeCell<T> {} static STATE: SyncUnsafeCell<GlobalState> = SyncUnsafeCell(UnsafeCell::new(GlobalState::new())); fn set_value(value: i32) { with_interrupts_disabled(|| { let state = STATE.0.get(); unsafe { // SAFETY: This value is only ever read in our interrupt handler, // and interrupts are disabled, and we only use this in one thread. (*state).value = value; } }); } }
标准库有一个仅在 nightly 版本中提供的(不稳定的)UnsafeCell
变体,称为 SyncUnsafeCell
。上面的示例显示了标准库类型的非常简化的版本,但使用方式大致相同。它可以提供更好的隔离,因此请查看其实现以了解更多详细信息。
此示例包括一个虚构的 with_interrupts_disabled
函数,这可能是您在嵌入式环境中看到的那种东西。例如,critical-section
crate 提供了类似的功能,可以用于嵌入式环境。
安全引用
在某些情况下,创建 static mut
的引用可能是安全的。static_mut_refs
lint 的全部意义在于,这很难正确地做到!但是,这并不是说这是不可能的。如果您的情况可以保证别名要求得到维护,例如保证静态变量的作用域很窄(仅在小型模块或函数中使用)、具有一些内部或外部同步、考虑了中断处理程序和重入、panic 安全性、drop 处理程序等,那么获取引用可能是可以的。
您可以采取两种方法来解决此问题。您可以允许 static_mut_refs
lint(最好尽可能缩小范围),或者将原始指针转换为引用,例如 &mut *&raw mut MY_STATIC
。
短生命周期引用
如果您必须创建 static mut
的引用,则建议尽量缩小该引用存在的范围。避免将引用偷偷地放在某个地方,或使其在代码的很大一部分中保持活动状态。保持其短生命周期有助于审计,并验证在整个持续时间内都维护了独占访问权限。使用指针应作为您的默认单元,并且仅在绝对必要时才按需将指针转换为引用。
迁移
没有自动迁移来修复这些对 static mut
的引用。为避免未定义行为,您必须重写代码以使用 替代方案 部分中推荐的不同方法。