Series: The Rust Annals
Vol. I Issue 83 nlopes.dev
Announcing Rust 1.82.0
Rust 1.82 combines substantial language and unsafe-code changes with defined NaN semantics, const floating point, and major tooling and platform improvements.
Rust 1.82 adds precise lifetime capture for return-position impl Trait, native raw-pointer syntax, and explicit unsafe contracts for foreign declarations and attributes. It also defines portable limits for NaN behavior, enables floating-point operations in const fn, makes uninhabited-value matches more concise, adds cargo info, and raises Apple Silicon macOS to Tier 1 support.
Precise lifetime capture replaces opaque-type workarounds
Return-position impl Trait types capture generic parameters that their hidden types may use, which in turn affects borrow checking. Rust 1.82 supports use<..> within certain impl Trait bounds so code can state precisely which generic lifetime parameters are captured.
In Rust 2021 and earlier editions, opaque types on bare functions and on functions and methods of inherent impls do not capture lifetime parameters unless those lifetimes appear syntactically in the opaque type:
//@ edition: 2021
fn f(x: &()) -> impl Sized { x }
error[E0700]: hidden type for `impl Sized` captures lifetime that does not appear in bounds
--> src/main.rs:1:30
|
1 | fn f(x: &()) -> impl Sized { x }
| --- ---------- ^
| | |
| | opaque type defined here
| hidden type `&()` captures the anonymous lifetime defined here
|
help: add a `use<...>` bound to explicitly capture `'_`
|
1 | fn f(x: &()) -> impl Sized + use<'_> { x }
| +++++++++
The direct fix is:
fn f(x: &()) -> impl Sized + use<'_> { x }
This replaces the baroque and subtle Captures trick:
trait Captures<T: ?Sized> {}
impl<T: ?Sized, U: ?Sized> Captures<T> for U {}
fn f(x: &()) -> impl Sized + Captures<&'_ ()> { x }
The formerly suggested outlives trick, fn f(x: &()) -> impl Sized + '_ { x }, is equivalent to + use<'_> in that simple case. In real cases it can overconstrain the returned opaque type. For example:
struct Ctx<'cx>(&'cx u8);
fn f<'cx, 'a>(
cx: Ctx<'cx>,
x: &'a u8,
) -> impl Iterator<Item = &'a u8> + 'cx {
core::iter::once_with(move || {
eprintln!("LOG: {}", cx.0);
x
})
//~^ ERROR lifetime may not live long enough
}
The + 'cx cannot be removed because the hidden type uses that lifetime, while adding 'a: 'cx would impose a relationship that is not generally true. Writing + use<'cx, 'a> instead captures both lifetimes with the correct bounds.
The syntax cannot currently appear within traits or trait impls, where in-scope lifetime parameters are already captured by default, and it must list every in-scope generic type and const parameter. Rust 2024 will automatically capture all lifetime parameters in scope, so these examples will work without the syntax; there, use<..> provides a way to opt out of that default. See the edition-guide chapter and the broader Rust 2024 explanation.
Unsafe obligations become explicit at FFI and symbol boundaries
An incorrect signature in an extern block can cause undefined behavior. Rust 1.82 therefore allows unsafe extern, assigning responsibility for the declarations' correctness to the block's author. Individual items may be marked safe, while an item marked neither safe nor unsafe is conservatively treated as unsafe:
unsafe extern {
pub safe static TAU: f64;
pub safe fn sqrt(x: f64) -> f64;
pub unsafe fn strlen(p: *const u8) -> usize;
}
Here, callers can read TAU and call sqrt without an unsafe block. Future releases will encourage unsafe extern through lints, and Rust 2024 will require it. Details are in RFC 3484 and the edition guide.
Attributes that can undermine Rust's safety guarantees now have corresponding unsafe syntax:
#[unsafe(no_mangle)]
pub fn my_global_function() { }
This applies to no_mangle, link_section, and export_name. Their old forms remain accepted in Rust 1.82, might be linted against in the future, and will be hard errors in Rust 2024. See the unsafe-attributes guide.
Raw references create pointers without first creating invalid references
Unsafe code may handle dangling, misaligned, or otherwise invalid-for-reference pointers. With a repr(packed) field, using & or &mut would create a reference and cause undefined behavior even if it were immediately cast to a raw pointer. Native &raw const and &raw mut syntax now replaces the roles of std::ptr::addr_of! and std::ptr::addr_of_mut!:
#[repr(packed)]
struct Packed {
not_aligned_field: i32,
}
fn main() {
let p = Packed { not_aligned_field: 1_82 };
// This would be undefined behavior!
// It is rejected by the compiler.
//let ptr = &p.not_aligned_field as *const i32;
// This is the old way of creating a pointer.
let ptr = std::ptr::addr_of!(p.not_aligned_field);
// This is the new way.
let ptr = &raw const p.not_aligned_field;
// Accessing the pointer has not changed.
// Note that `val = *ptr` would be undefined behavior because
// the pointer is not aligned!
let val = unsafe { ptr.read_unaligned() };
}
The syntax makes clear that its operand is interpreted as a place expression and avoids equating a pointer with merely an address; a pointer is more than an address.
Creating raw pointers to mutable and foreign statics is also now allowed without an unsafe block because this operation does not read or write the place:
static mut STATIC_MUT: Type = Type::new();
extern "C" {
static EXTERN_STATIC: Type;
}
fn main() {
let static_mut_ptr = &raw mut STATIC_MUT;
let extern_static_ptr = &raw const EXTERN_STATIC;
}
Code denying unused_unsafe may now reject an unsafe block retained only for older compilers. Multi-version code can annotate that block:
static mut STATIC_MUT: Type = Type::new();
fn main() {
+ #[allow(unused_unsafe)]
let static_mut_ptr = unsafe { std::ptr::addr_of_mut!(STATIC_MUT) };
}
A future Rust version is expected to generalize this relaxation to other expressions that would be safe in this position, not only statics.
NaN rules unlock const floating point, while empty matches and assembly gain precision
Rust now specifies rules for the NaN values produced by f32 and f64 operations. NaNs have signs and payloads observable through methods such as is_sign_positive and to_bits, although == ignores both and always returns false for NaN. Hardware differences and optimizations such as replacing f * 1.0 with f can change the resulting NaN bit pattern.
The new rules are deliberately not fully deterministic: (0.0 / 0.0).is_sign_positive() can vary with the hardware architecture, optimization level, and surrounding code. Fully portable code should avoid to_bits and use f.signum() == 1.0 instead of f.is_sign_positive(). The rules still permit techniques such as NaN boxing; the exact rules are in the floating-point documentation.
With those semantics settled, floating-point operations may now be used in const fn. An operation such as (0.0 / 0.0).is_sign_positive()—whose method will become const-stable in Rust 1.83—can produce different compile-time and run-time results. This is not a bug, and code must not rely on a const fn always producing the exact same result.
Patterns matching empty, or uninhabited, types by value may now be omitted:
use std::convert::Infallible;
pub fn unwrap_without_panic<T>(x: Result<T, Infallible>) -> T {
let Ok(x) = x; // the `Err` case does not need to appear
x
}
This works for a variant-less enum Void {} and for structs or enums with a visible empty field and no #[non_exhaustive] attribute. It will also be useful with the still-unstable never type !. For reasons involving uninitialized values and unsafe code, the empty case cannot be omitted when accessed through a reference, pointer, or union field:
pub fn unwrap_ref_without_panic<T>(x: &Result<T, Infallible>) -> &T {
match x {
Ok(x) => x,
// this arm cannot be omitted because of the reference
Err(infallible) => match *infallible {},
}
}
To avoid disrupting crates that support multiple Rust versions, such arms are not yet reported as unreachable-code warnings even though they can be removed.
The inline-assembly const operand can now place integers directly into immediates instead of first storing them in registers. In this example, LEN = const MSG.len() fills the LEN format specifier with the immediate value 14:
const WRITE_SYSCALL: c_int = 0x01; // syscall 1 is `write`
const STDOUT_HANDLE: c_int = 0x01; // `stdout` has file handle 1
const MSG: &str = "Hello, world!\n";
let written: usize;
// Signature: `ssize_t write(int fd, const void buf[], size_t count)`
unsafe {
core::arch::asm!(
"mov rax, {SYSCALL} // rax holds the syscall number",
"mov rdi, {OUTPUT} // rdi is `fd` (first argument)",
"mov rdx, {LEN} // rdx is `count` (third argument)",
"syscall // invoke the syscall",
"mov {written}, rax // save the return value",
SYSCALL = const WRITE_SYSCALL,
OUTPUT = const STDOUT_HANDLE,
LEN = const MSG.len(),
in("rsi") MSG.as_ptr(), // rsi is `buf *` (second argument)
written = out(reg) written,
);
}
assert_eq!(written, MSG.len());
Hello, world!
lea rsi, [rip + .L__unnamed_3]
mov rax, 1 # rax holds the syscall number
mov rdi, 1 # rdi is `fd` (first argument)
mov rdx, 14 # rdx is `count` (third argument)
syscall # invoke the syscall
mov rax, rax # save the return value
See the complete example and inline-assembly reference.
Cargo package inspection and Apple platform support improve
Cargo now includes an info subcommand for registry package information, fulfilling a request nearly ten years old. After several third-party implementations, this version was developed as cargo-information before joining Cargo.
For cargo info cc, the output includes the package description, selected and latest versions, license, Rust-version requirement, project links, features, and a command for finding reverse dependencies:
cc #build-dependencies
A build-time dependency for Cargo build scripts to assist in invoking the native
C compiler to compile native C code into a static archive to be linked into Rust
code.
version: 1.1.23 (latest 1.1.30)
license: MIT OR Apache-2.0
rust-version: 1.63
documentation: https://docs.rs/cc
homepage: https://github.com/rust-lang/cc-rs
repository: https://github.com/rust-lang/cc-rs
crates.io: https://crates.io/crates/cc/1.1.23
features:
jobserver = []
parallel = [dep:libc, dep:jobserver]
note: to see how you depend on cc, run `cargo tree --invert --package cc@1.1.23`
By default, cargo info reports the version in the local Cargo.lock, when present, and indicates whether a newer version exists. A version-qualified request such as cargo info cc@1.1.30 reports that version instead.
The aarch64-apple-darwin target for M1-family and later Apple Silicon Macs is now Tier 1, Rust's highest support guarantee. Every Rust repository change must pass the full test suite on every Tier 1 target before merging. Introduced at Tier 2 in Rust 1.49, it now joins 64-bit ARM Linux and the X86 macOS, Linux, and Windows targets at Tier 1.
The Mac Catalyst targets aarch64-apple-ios-macabi and x86_64-apple-ios-macabi are now Tier 2. Catalyst runs iOS applications natively on macOS, so cargo test --target=aarch64-apple-ios-macabi --target=x86_64-apple-ios-macabi mostly works without the external bundling required to run usual iOS targets on devices or simulators. The targets are available through rustup target add aarch64-apple-ios-macabi x86_64-apple-ios-macabi, making this a good time to add iOS-like environments to CI.
The standard-library inventory below also includes newly stable allocation, iterator, collection, architecture-intrinsic, task, string, option, threading, and integer-parsing interfaces. Its final five entries are newly usable in const contexts.