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;
let ref x = X;
let (x, y) = (&X, &Y);
}
}
仅仅获取这样的引用就违反了 Rust 的可变性 XOR 别名要求,这始终是即时的 未定义行为 ,即使该引用从未被读取或写入 。此外,为了维护 static mut
的可变性 XOR 别名,需要全局性地推理您的代码 ,这在面对重入和/或多线程时可能尤其困难。
请注意,在某些情况下,隐式引用会自动创建,而没有可见的 &
运算符。例如,以下情况也会触发 lint
#![allow(unused)]
fn main () {
static mut NUMS: &[u8 ; 3 ] = &[0 , 1 , 2 ];
unsafe {
println! ("{NUMS:?}" );
let n = NUMS.len();
}
}
在任何可能的情况下,强烈建议 改用不可变的 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;
static COUNTER: AtomicU64 = AtomicU64::new(0 );
fn main () {
COUNTER.fetch_add(1 , Ordering::Relaxed);
}
当您的类型比原子类型更复杂时,请考虑使用 Mutex
或 RwLock
来确保对全局值的正确访问。
use std::sync::Mutex;
use std::collections::VecDeque;
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();
}
如果您因为需要执行一些不能是 const
的一次性初始化而使用 static mut
,您可以改为使用 OnceLock
或 LazyLock
。
use std::sync::LazyLock;
struct GlobalState ;
impl GlobalState {
fn new () -> GlobalState {
GlobalState
}
fn example (&self ) {}
}
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();
}
此示例类似于 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()
{
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 {
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 {
example_ffi(&raw mut STATE);
}
}
请注意,您仍然需要维护围绕可变指针的别名约束。这可能需要一些内部或外部同步,或者关于如何在线程、中断处理程序和重入之间使用它的证明。
UnsafeCell
未实现 Sync
,因此不能在 static
中使用。您可以围绕 UnsafeCell
创建自己的包装器以添加 Sync
实现,以便它可以在 static
中用于实现内部可变性。如果您有外部锁或其他保证来维护可变指针所需的安全不变量,则此方法可能很有用。
请注意,这在很大程度上与 原始指针 示例相同。包装器有助于强调您如何使用该类型,并专注于您应注意哪些安全要求。但除此之外,它们大致相同。
#![allow(unused)]
fn main () {
use std::cell::UnsafeCell;
fn with_interrupts_disabled <T: Fn ()>(f: T) {
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 {
(*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
的引用。为避免未定义行为,您必须重写代码以使用 替代方案 部分中推荐的不同方法。