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>orRwLock<T>for synchronized interior mutability.OnceLock<T>orLazyLock<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 constor&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"]