Rust 1.82.0 发布公告

2024 年 10 月 17 日 · Rust 发布团队

Rust 团队很高兴地宣布 Rust 新版本 1.82.0 发布。Rust 是一种编程语言,旨在赋能所有人构建可靠且高效的软件。

如果您之前通过 rustup 安装了 Rust,您可以使用以下命令获取 1.82.0 版本:

$ rustup update stable

如果您尚未安装,您可以从我们的网站上的相应页面获取 rustup,并查看1.82.0 版本的详细发布说明

如果您想通过测试未来的版本来帮助我们,您可以考虑在本地更新以使用 beta 频道 (rustup default beta) 或 nightly 频道 (rustup default nightly)。请报告您可能遇到的任何错误!

1.82.0 稳定版的新特性

cargo info

Cargo 现在有了一个 info 子命令,用于显示注册表中软件包的信息,满足了一个长期以来的请求,这个请求已经快十年了!多年来,已经编写了几个像这样的第三方扩展,这个实现最初是作为 cargo-information 开发的,然后合并到了 Cargo 本身。

例如,以下是您可能会看到的 cargo info cc 的输出

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`

默认情况下,cargo info 描述本地 Cargo.lock 中的软件包版本(如果有)。如您所见,它也会指示何时有更新的版本,而 cargo info cc@1.1.30 将报告该版本的信息。

Apple 目标提升

macOS on 64 位 ARM 现在是 Tier 1

用于 64 位 ARM (M1 系列或更高版本的 Apple Silicon CPU) macOS 的 Rust 目标 aarch64-apple-darwin 现在是 Tier 1 目标,表明我们对其正常工作提供最高保证。正如平台支持页面所述,Rust 存储库中的每个更改都必须在每个 Tier 1 目标上通过完整测试后才能合并。此目标在 Rust 1.49 中作为 Tier 2 引入,使其在 rustup 中可用。这个新的里程碑使 aarch64-apple-darwin 目标与 64 位 ARM Linux 以及 X86 macOS、Linux 和 Windows 目标处于同等水平。

Mac Catalyst 目标现在是 Tier 2

Mac Catalyst 是 Apple 的一项技术,允许在 Mac 上原生运行 iOS 应用程序。这在测试特定于 iOS 的代码时特别有用,因为 cargo test --target=aarch64-apple-ios-macabi --target=x86_64-apple-ios-macabi 大部分情况下都可以正常工作(与通常的 iOS 目标相反,后者需要使用外部工具进行捆绑才能在原生设备或模拟器中运行)。

这些目标现在是 Tier 2,可以使用 rustup target add aarch64-apple-ios-macabi x86_64-apple-ios-macabi 下载,因此现在是更新您的 CI 管道以测试您的代码是否也在类似 iOS 的环境中运行的绝佳时机。

精确捕获 use<..> 语法

Rust 现在在某些 impl Trait 边界内支持 use<..> 语法,以控制捕获哪些泛型生命周期参数。

Rust 中的返回位置 impl Trait (RPIT) 类型捕获某些泛型参数。捕获泛型参数允许该参数在隐藏类型中使用。这反过来会影响借用检查。

在 Rust 2021 及更早版本中,除非生命周期参数在不透明类型中以语法方式提及,否则生命周期参数不会在裸函数以及固有 impl 的函数和方法中的不透明类型中捕获。例如,这是一个错误

//@ 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 }
  |                            +++++++++

使用新的 use<..> 语法,我们可以按照错误提示建议的那样修复此问题,方法是编写

fn f(x: &()) -> impl Sized + use<'_> { x }

以前,正确修复此类错误需要定义一个虚拟特征,通常称为 Captures,并按如下方式使用它

trait Captures<T: ?Sized> {}
impl<T: ?Sized, U: ?Sized> Captures<T> for U {}

fn f(x: &()) -> impl Sized + Captures<&'_ ()> { x }

这被称为Captures 技巧”,它有点巴洛克式且微妙。现在不再需要了。

有一种不太正确但更方便的方法来修复这个问题,通常被称为“outlives 技巧”。编译器甚至以前也建议这样做。该技巧看起来像这样

fn f(x: &()) -> impl Sized + '_ { x }

在这种简单的情况下,该技巧与 + use<'_> 完全等效,其微妙原因在 RFC 3498 中进行了解释。但是,在实际情况下,这会过度约束返回的不透明类型的边界,从而导致问题。例如,考虑以下代码,它受到 Rust 编译器中一个真实案例的启发

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
}

我们不能删除 + 'cx,因为生命周期在隐藏类型中使用,因此必须捕获。我们也不能添加 'a: 'cx 的边界,因为这些生命周期实际上不相关,并且通常情况下 'a 不会超出 'cx 的生命周期。但是,如果我们写 + use<'cx, 'a>,这将起作用并具有正确的边界。

我们今天稳定的功能有一些限制。use<..> 语法目前不能出现在特征内或特征 impl 内(但请注意,在特征内,作用域内的生命周期参数已默认捕获),并且它必须列出所有作用域内的泛型类型和 const 参数。我们希望随着时间的推移解除这些限制。

请注意,在 Rust 2024 中,上面的示例将“直接工作”,而无需 use<..> 语法(或任何技巧)。这是因为在新版本中,不透明类型将自动捕获作用域内的所有生命周期参数。这是一个更好的默认设置,我们已经看到了很多证据表明这如何清理代码。在 Rust 2024 中,use<..> 语法将作为一种重要的选择退出默认设置的方式。

有关 use<..> 语法、捕获以及这如何应用于 Rust 2024 的更多详细信息,请参阅版本指南的“RPIT 生命周期捕获规则”章节。有关总体方向的详细信息,请参阅我们最近的博客文章“Rust 2024 中 impl Trait 的更改”

用于创建原始指针的本地语法

不安全代码有时必须处理可能悬空、可能未对齐或可能未指向有效数据的指针。这种情况常见于 repr(packed) 结构体。在这种情况下,重要的是避免创建引用,因为这会导致未定义的行为。这意味着不能使用通常的 &&mut 运算符,因为它们会创建引用——即使引用立即转换为原始指针,也为时已晚,无法避免未定义的行为。

多年来,宏 std::ptr::addr_of!std::ptr::addr_of_mut! 一直用于此目的。现在是时候为此操作提供适当的本地语法了:addr_of!(expr) 变为 &raw const expr,而 addr_of_mut!(expr) 变为 &raw mut expr。例如

#[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() };
}

本地语法更清楚地表明这些运算符的操作数表达式被解释为位置表达式。它还避免了在指创建指针的操作时使用“address-of”术语。指针不仅仅是一个地址,因此 Rust 正在远离像“address-of”这样的术语,这些术语重新肯定了指针和地址之间的错误等价性。

带有 unsafe extern 的安全项

Rust 代码可以使用来自外部代码的函数和静态变量。这些外部项的类型签名在 extern 块中提供。从历史上看,extern 块中的所有项都是不安全使用的,但是我们不必在 extern 块本身上写任何 unsafe

但是,如果 extern 块中的签名不正确,则使用该项将导致未定义的行为。这应该是编写 extern 块的人的错,还是使用该项的人的错?

我们已决定,确保 extern 块中包含的所有签名都是正确的责任在于编写 extern 块的人,因此我们现在允许编写 unsafe extern

unsafe extern {
    pub safe static TAU: f64;
    pub safe fn sqrt(x: f64) -> f64;
    pub unsafe fn strlen(p: *const u8) -> usize;
}

这样做的一个好处是,unsafe extern 块中的项可以标记为安全使用。在上面的示例中,我们可以调用 sqrt 或读取 TAU 而无需使用 unsafe。未标记为 safeunsafe 的项保守地假定为 unsafe

在未来的版本中,我们将鼓励使用带有 lint 的 unsafe extern。从 Rust 2024 开始,将需要使用 unsafe extern

有关更多详细信息,请参阅 RFC 3484 和版本指南的“Unsafe extern blocks”章节。

不安全属性

某些 Rust 属性,例如 no_mangle,可以用于在没有任何 unsafe 块的情况下导致未定义的行为。如果这是常规代码,我们将要求将它们放在 unsafe {} 块中,但到目前为止,属性还没有可比较的语法。为了反映这些属性可能会破坏 Rust 的安全保证这一事实,它们现在被认为是“不安全”的,应该按如下方式编写

#[unsafe(no_mangle)]
pub fn my_global_function() { }

旧形式的属性(没有 unsafe)目前仍然被接受,但在未来的某个时候可能会被 lint 警告,并且在 Rust 2024 中将成为硬错误。

这会影响以下属性

  • no_mangle
  • link_section
  • export_name

有关更多详细信息,请参阅版本指南的“Unsafe attributes”章节。

在模式匹配中省略空类型

现在可以省略按值匹配空类型(也称为 uninhabited 类型)的模式

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
}

这适用于空类型,例如无变体的 enum Void {},或具有可见空字段且没有 #[non_exhaustive] 属性的结构体和枚举。它也特别适用于 never 类型 !,尽管该类型目前仍不稳定。

在某些情况下,仍然必须编写空模式。由于与未初始化值和不安全代码相关的原因,如果通过引用、指针或 union 字段访问空类型,则不允许省略模式

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 {},
    }
}

为了避免干扰希望支持多个 Rust 版本的 crate,尽管事实上可以删除带有空模式的 match arm,但它们尚未报告为“无法访问的代码”警告。

浮点 NaN 语义和 const

对浮点值(f32f64 类型)的操作以其微妙性而闻名。其中一个原因是 NaN(“非数字”)值的存在,它用于表示例如 0.0 / 0.0 的结果。使 NaN 值微妙之处在于存在多个可能的 NaN 值。NaN 值有一个符号(可以使用 f.is_sign_positive() 检查)和一个有效负载(可以使用 f.to_bits() 提取)。但是,NaN 值的符号和有效负载都被 == 完全忽略(它始终返回 false)。尽管在标准化跨硬件架构的浮点运算行为方面取得了非常成功的努力,但 NaN 为正还是负以及其确切有效负载的细节在不同架构之间有所不同。更复杂的是,当保证确切的数值结果不会改变时,Rust 及其 LLVM 后端会对浮点运算应用优化,但这些优化可能会改变生成的 NaN 值。例如,f * 1.0 可能会优化为仅 f。但是,如果 f 是 NaN,这可能会更改结果的确切位模式!

在此版本中,Rust 标准化了一组关于 NaN 值行为的规则。这组规则不是完全确定性的,这意味着像 (0.0 / 0.0).is_sign_positive() 这样的操作结果可能会因硬件架构、优化级别和周围代码而异。旨在完全可移植的代码应避免使用 to_bits,并应使用 f.signum() == 1.0 而不是 f.is_sign_positive()。但是,仔细选择这些规则仍然允许在 Rust 代码中实现高级数据表示技术,例如 NaN boxing。有关确切规则的更多详细信息,请查看我们的文档

随着 NaN 值语义的确定,此版本还允许在 const fn 中使用浮点运算。由于上述原因,像 (0.0 / 0.0).is_sign_positive() 这样的操作(在 Rust 1.83 中将是 const-stable 的)在编译时与运行时执行时可能会产生不同的结果。这不是错误,代码不得依赖于 const fn 始终产生完全相同的结果。

常量作为汇编立即数

const 汇编操作数现在提供了一种使用整数作为立即数的方法,而无需先将它们存储在寄存器中。例如,我们手动实现了一个调用 write 的系统调用

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!

Playground 链接.

在上面,诸如 LEN = const MSG.len() 之类的语句使用取值 MSG.len() 的立即数填充格式说明符 LEN。这可以在生成的汇编中看到(该值为 14

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

有关更多详细信息,请参阅参考

安全地寻址不安全的 static 变量

现在允许使用以下代码

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

在表达式上下文中,STATIC_MUTEXTERN_STATIC位置表达式。以前,编译器的安全检查器不知道原始引用运算符实际上不影响操作数的位置,将其视为可能读取或写入指针。但是,实际上不存在不安全性,因为它只是创建了一个指针。

放宽此限制可能会导致某些不安全块现在报告为未使用,如果您拒绝 unused_unsafe lint,但它们现在仅在旧版本上有用。如果您希望支持多个 Rust 版本,请使用 #[allow(unused_unsafe)] 注释这些不安全块,如本示例 diff 中所示

 static mut STATIC_MUT: Type = Type::new();
 fn main() {
+    #[allow(unused_unsafe)]
     let static_mut_ptr = unsafe { std::ptr::addr_of_mut!(STATIC_MUT) };
 }

预计未来的 Rust 版本将推广到其他在这种位置安全的表达式,而不仅仅是静态变量。

稳定的 API

这些 API 现在在 const 上下文中是稳定的

其他更改

查看 RustCargoClippy 中更改的所有内容。

1.82.0 版本贡献者

许多人齐心协力创建了 Rust 1.82.0。没有你们所有人,我们不可能做到。谢谢!