Rust 2024 Edition

Edition Migration

Edition migration upgrades an existing crate or workspace from an earlier edition (such as 2018 or 2021) to edition = "2024". Because every edition change is designed together with automated compiler migration lints, upgrading a crate is a deterministic, tool-assisted process.

Summary of Key Changes When Migrating from Rust 2021 to Rust 2024:

  1. Language & Type System:
     - `impl Trait` return types capture all in-scope lifetimes by default (`use<..>` to restrict)
     - Reserved keyword `gen` (existing `gen` identifiers become raw identifiers `r#gen`)
     - Match ergonomics disallow redundant `ref` / `mut` when matching by implicit reference
     - Temporary lifetimes inside `if let` / `match` scrutinees drop earlier to prevent deadlocks
     - `!` (Never type) fallback changes from `()` to `!`

  2. Unsafe & Soundness Rules:
     - `extern "C" { ... }` blocks must be written `unsafe extern "C" { ... }`
     - `#[no_mangle]`, `#[export_name]`, `#[link_section]` require `#[unsafe(...)]`
     - `std::env::set_var` and `remove_var` are now `unsafe fn`
     - `unsafe_op_in_unsafe_fn` lint is `warn` by default
     - References to `static mut` (`&STATIC_MUT`) are a hard error (`static_mut_refs`)

  3. Standard Library & Tooling:
     - `Future` and `IntoFuture` added to the standard prelude
     - `Box<[T]>::into_iter()` iterates by value (`T`) instead of `&T`
     - Cargo defaults to MSRV-aware Resolver v3 (`resolver = "3"`)
     - `rustdoc` merges doc-tests into a single binary

Disallowing Direct References to static mut (static_mut_refs)

One of the most important soundness upgrades in Rust 2024 is turning the static_mut_refs lint into a hard error (deny by default). Creating a shared or mutable reference (&COUNTER or &mut COUNTER) to a static mut variable is almost impossible to do soundly because any concurrent or reentrant access creates immediate aliasing Undefined Behavior!

In Rust 2024, replace static mut with:

  • Atomic* (AtomicU64, AtomicBool, AtomicUsize) for lock-free primitives.
  • Mutex<T> or RwLock<T> for synchronized interior mutability.
  • OnceLock<T> or LazyLock<T> for one-time initialization.
  • Or, in single-threaded FFI/embedded code that truly needs a raw pointer to a static buffer, use raw pointer operators (&raw const or &raw mut) without ever creating a Rust reference:
// ❌ does not compile
static mut GLOBAL_BUFFER: [u8; 4] = [1, 2, 3, 4];

fn main() {
    // Creating a reference (`&` or `&mut`) to a `static mut` is denied in Rust 2024!
    let slice = unsafe { &GLOBAL_BUFFER };
    println!("{slice:?}");
}
error: creating a shared reference to mutable static is discouraged
 --> src/main.rs:5:26
  |
5 |     let slice = unsafe { &GLOBAL_BUFFER };
  |                          ^^^^^^^^^^^^^^ shared reference to mutable static
  |
  = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/static-mut-references.html>
  = note: shared references to mutable statics are dangerous; it's undefined behavior if the static is mutated or if a mutable reference is created for it while the shared reference lives
  = note: `#[deny(static_mut_refs)]` on by default

Replacing static mut with AtomicUsize or Mutex eliminates unsafe completely:

use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};

static REQUEST_COUNT: AtomicUsize = AtomicUsize::new(0);
static LOG_ENTRIES: Mutex<Vec<&'static str>> = Mutex::new(Vec::new());

fn main() {
    REQUEST_COUNT.fetch_add(1, Ordering::Relaxed);
    LOG_ENTRIES.lock().unwrap().push("server_started");

    println!(
        "Requests: {}, Logs: {:?}",
        REQUEST_COUNT.load(Ordering::Relaxed),
        *LOG_ENTRIES.lock().unwrap()
    );
}
Requests: 1, Logs: ["server_started"]