禁止引用 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);
}

互斥锁或读写锁

当您的类型比原子类型更复杂时,请考虑使用 MutexRwLock 来确保对全局值的正确访问。

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,您可以改为使用 OnceLockLazyLock

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);
    }
}

请注意,您仍然需要维护围绕可变指针的别名约束。这可能需要一些内部或外部同步,或者关于如何在线程、中断处理程序和重入之间使用它的证明。

带有 SyncUnsafeCell

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 的引用。为避免未定义行为,您必须重写代码以使用 替代方案 部分中推荐的不同方法。