Rust 1.82.0 发布 (译文)

本篇为译文,来源: https://blog.rust-lang.org/2024/10/17/Rust-1.82.0.html

2024 年 10 月 17 日 · The Rust Release Team

Rust 团队很高兴宣布推出新版本 Rust 1.82.0。Rust 是一种编程语言,可帮助每个人构建可靠、高效的软件。

如果您已通过 安装了旧版本的 Rust rustup,则可以通过以下方式获取 1.82.0:

1$ rustup update stable

如果您还没有,您可以从我们网站的相应页面获取rustup,并查看1.82.0 的详细发行说明

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

1.82.0 稳定版包含哪些内容

cargo info

Cargo 现在有一个info子命令来显示有关注册表中包的信息,这满足了其十周年纪念日前的一个长期请求!多年来,已经编写了几个类似的第三方扩展,这个实现在合并到 Cargo 本身之前被开发为Cargo-information

例如,以下是您能看到的内容cargo info cc

 1cc #build-dependencies
 2A build-time dependency for Cargo build scripts to assist in invoking the native
 3C compiler to compile native C code into a static archive to be linked into Rust
 4code.
 5version: 1.1.23 (latest 1.1.30)
 6license: MIT OR Apache-2.0
 7rust-version: 1.63
 8documentation: https://docs.rs/cc
 9homepage: https://github.com/rust-lang/cc-rs
10repository: https://github.com/rust-lang/cc-rs
11crates.io: https://crates.io/crates/cc/1.1.23
12features:
13  jobserver = []
14  parallel  = [dep:libc, dep:jobserver]
15note: 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 目标促销

64 位 ARM 上的 macOS 现已成为第 1 级

aarch64-apple-darwin64 位 ARM(M1 系列或更高版本的 Apple Silicon CPU)上的 macOS 的Rust 目标现在是第 1 级目标,表明我们最高程度地保证正常运行。正如平台支持页面所述,Rust 存储库中的每个更改都必须在每个第 1 级目标上通过完整测试,然后才能合并。此目标早在 Rust 1.49 中就作为第 2 级引入,使其在 中可用rustup。这一新里程碑使该aarch64-apple-darwin目标与 64 位 ARM Linux 和 X86 macOS、Linux 和 Windows 目标相提并论。

Mac Catalyst 目标现在为第 2 层

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

目标现在是第 2 层,可以通过 下载rustup target add aarch64-apple-ios-macabi x86_64-apple-ios-macabi,所以现在是更新 CI 管道以测试您的代码是否也可以在类似 iOS 的环境中运行的绝佳时机。

精确捕获use<..>语法

Rust 现在支持use<..>一定范围内的语法impl Trait来控制捕获哪些通用生命周期参数。

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

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

 1//@ edition: 2021
 2fn f(x: &()) -> impl Sized { x }
 3error[E0700]: hidden type for `impl Sized` captures lifetime that does not appear in bounds
 4 --> src/main.rs:1:30
 5  |
 61 | fn f(x: &()) -> impl Sized { x }
 7  |         ---     ----------   ^
 8  |         |       |
 9  |         |       opaque type defined here
10  |         hidden type `&()` captures the anonymous lifetime defined here
11  |
12help: add a `use<...>` bound to explicitly capture `'_`
13  |
141 | fn f(x: &()) -> impl Sized + use<'_> { x }
15  |                            +++++++++

使用新的use<..>语法,我们可以修复这个问题,正如错误中所建议的,通过编写:

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

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

1trait Captures<T: ?Sized> {}
2impl<T: ?Sized, U: ?Sized> Captures<T> for U {}
3
4fn f(x: &()) -> impl Sized + Captures<&'_ ()> { x }

这就是所谓的Captures诡计”,有点儿复杂和微妙。现在不再需要了。

有一种不太正确但更方便的方法来解决这个问题,这种方法经常被使用,称为“outlives 技巧”。编译器甚至以前就建议这样做。这个技巧如下所示:

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

在这个简单的例子中,这个技巧与RFC 3498+ use<'_>中解释的微妙原因完全相同。然而,在现实生活中,这会过度限制返回的不透明类型的界限,从而导致问题。例如,考虑以下代码,它受到 Rust 编译器中真实案例的启发:

 1struct Ctx<'cx>(&'cx u8);
 2
 3fn f<'cx, 'a>(
 4    cx: Ctx<'cx>,
 5    x: &'a u8,
 6) -> impl Iterator<Item = &'a u8> + 'cx {
 7    core::iter::once_with(move || {
 8        eprintln!("LOG: {}", cx.0);
 9        x
10    })
11//~^ ERROR lifetime may not live long enough
12}

我们不能删除+ 'cx,因为 生命周期用于隐藏类型,因此必须捕获。我们也不能添加 的界限'a: 'cx,因为这些生命周期实际上并不相关,并且通常不会是 比'a长寿'cx+ use<'cx, 'a>但是,如果我们改为这样写,这将起作用并具有正确的界限。

我们目前正在稳定的内容存在一些限制。use<..>语法目前不能出现在特征或特征实现中(但请注意,范围内的生命周期参数已默认被捕获),并且它必须列出所有范围内的泛型类型和 const 参数。我们希望随着时间的推移取消这些限制。

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

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

创建原始指针的本机语法

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

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

 1#[repr(packed)]
 2struct Packed {
 3    not_aligned_field: i32,
 4}
 5
 6fn main() {
 7    let p = Packed { not_aligned_field: 1_82 };
 8
 9    // This would be undefined behavior!
10    // It is rejected by the compiler.
11    //let ptr = &p.not_aligned_field as *const i32;
12
13    // This is the old way of creating a pointer.
14    let ptr = std::ptr::addr_of!(p.not_aligned_field);
15
16    // This is the new way.
17    let ptr = &raw const p.not_aligned_field;
18
19    // Accessing the pointer has not changed.
20    // Note that `val = *ptr` would be undefined behavior because
21    // the pointer is not aligned!
22    let val = unsafe { ptr.read_unaligned() };
23}

原生语法更清楚地表明,这些运算符的操作数表达式被解释为位置表达式。它还避免在指代创建指针的操作时使用术语“地址”。指针不仅仅是一个地址,因此 Rust 正在远离“地址”等重申指针和地址错误等价的术语。

安全物品unsafe extern

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

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

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

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

这样做的一个好处是unsafe extern可以将块中的项标记为可以安全使用。在上面的例子中,我们可以不使用 来调用sqrt或读取。未标记为或 的项保守地被认为是。TAU``unsafe``safe``unsafe``unsafe

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

有关更多详细信息,请参阅RFC 3484和版本指南的“不安全的外部块”章节。

不安全的属性

某些 Rust 属性,例如 no_mangle,在存在名称冲突时可能导致未定义行为。如果这些属性用于常规代码,我们通常会要求将它们放在 unsafe {} 块中,但目前尚无类似的语法。为了反映这些属性可能破坏 Rust 的安全保证,它们现在被标记为“不安全的”,应写作如下形式:

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

旧的属性形式(不带 unsafe)仍然被接受,但未来可能会被弃用,并在 Rust 2024 中成为硬错误。

受此影响的属性包括:

  • no_mangle
  • link_section
  • export_name

有关更多详细信息,请参阅版本指南中的“不安全属性”章节

在模式匹配中省略空类型

现在可以在值匹配中省略空(无人居住)类型的模式:

1use std::convert::Infallible;
2
3pub fn unwrap_without_panic<T>(x: Result<T, Infallible>) -> T {
4    let Ok(x) = x; // `Err` 分支可以省略
5    x
6}

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

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

1pub fn unwrap_ref_without_panic<T>(x: &Result<T, Infallible>) -> &T {
2    match x {
3        Ok(x) => x,
4        // 这个分支不能省略,因为它是引用
5        Err(infallible) => match *infallible {},
6    }
7}

为了避免干扰需要支持多个 Rust 版本的库,尽管可以省略具有空模式的分支,但它们尚未报告为“不可达代码”警告。

浮点 NaN 语义与 const

对浮点值(类型为 f32f64)的运算非常微妙,其中一个原因是存在 NaN(“非数字”)值,用于表示如 0.0 / 0.0 的结果。NaN 值之所以复杂,是因为存在多个可能的 NaN 值。NaN 值有一个符号(可用 f.is_sign_positive() 检查)和一个有效载荷(可用 f.to_bits() 提取)。然而,NaN 值的符号和有效载荷都被完全忽略 ==(始终返回 false)。尽管在标准化硬件架构中对浮点运算行为进行了成功的努力,但 NaN 的具体正负和有效载荷在不同架构之间有所不同。

为了简化情况,Rust 在本版本中标准化了一组 NaN 值行为的规则。这些规则并非完全确定,这意味着像 (0.0 / 0.0).is_sign_positive() 这样的操作结果可能因硬件架构、优化级别和上下文代码不同而有所不同。旨在完全可移植的代码应避免使用 to_bits,而应使用 f.signum() == 1.0 替代 f.is_sign_positive()。这些规则经过精心选择,仍然允许在 Rust 代码中实现 NaN 装箱等高级数据表示技术。有关具体规则的更多详细信息,请查看我们的文档

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

常量作为汇编立即数

汇编中的 const 操作数现在允许使用整数作为立即数,而无需先将它们存储在寄存器中。例如,我们可以手动实现一个系统调用来执行 write

 1const WRITE_SYSCALL: c_int = 0x01; // syscall 1 is `write`
 2const STDOUT_HANDLE: c_int = 0x01; // `stdout` has file handle 1
 3const MSG: &str = "Hello, world!\n";
 4
 5let written: usize;
 6
 7// Signature: `ssize_t write(int fd, const void buf[], size_t count)`
 8unsafe {
 9    core::arch::asm!(
10        "mov rax, {SYSCALL} // rax holds the syscall number",
11        "mov rdi, {OUTPUT}  // rdi is `fd` (first argument)",
12        "mov rdx, {LEN}     // rdx is `count` (third argument)",
13        "syscall            // invoke the syscall",
14        "mov {written}, rax // save the return value",
15        SYSCALL = const WRITE_SYSCALL,
16        OUTPUT = const STDOUT_HANDLE,
17        LEN = const MSG.len(),
18        in("rsi") MSG.as_ptr(), // rsi is `buf *` (second argument)
19        written = out(reg) written,
20    );
21}
22
23assert_eq!(written, MSG.len());

输出:

1Hello, world!

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

1lea     rsi, [rip + .L__unnamed_3]
2mov     rax, 1    # rax holds the syscall number
3mov     rdi, 1    # rdi is `fd` (first argument)
4mov     rdx, 14   # rdx is `count` (third argument)
5syscall # invoke the syscall
6mov     rax, rax  # save the return value

请参阅参考资料以获取更多详细信息。

安全解决不安全 static 问题

现在允许以下代码:

1static mut STATIC_MUT: Type = Type::new();
2extern "C" {
3    static EXTERN_STATIC: Type;
4}
5
6fn main() {
7     let static_mut_ptr = &raw mut STATIC_MUT;
8     let extern_static_ptr = &raw const EXTERN_STATIC;
9}

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

放宽这一限制可能会导致一些问题。如果你禁用 unused_unsafe lint,某些不安全的块现在会被报告为未使用,但它们现在在旧版本中只会有用。如果希望支持多个 Rust 版本,请对这些不安全的块进行注释 #[allow(unused_unsafe)],如下所示:

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

未来的 Rust 版本可能会将此推广到在此位置上安全的其他表达式,而不仅限于静态变量。

稳定的 API

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

其他变化

检查RustCargoClippy中发生的所有变化。

1.82.0 的贡献者

许多人齐心协力创建了 Rust 1.82.0。如果没有你们所有人,我们就无法做到这一点。谢谢!