Rust 新版解读 | 1.82 | 超大更新

Rust 1.82 官方 release doc: Announcing Rust 1.82.0 | Rust Blog

通过 rustup 安装的同学可以使用以下命令升级到 1.82 版本:

$ rustup update stable

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-bit ARM 现在是 Tier 1

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

Mac Catalyst 目标现在是 Tier 2

Mac Catalyst 是苹果的一项技术,允许在 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 中的 Return-position impl Trait(RPIT)类型会捕获某些泛型参数。捕获一个泛型参数允许该参数在隐藏类型中使用。这反过来会影响借用检查。

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

#![allow(unused)]
fn main() {
//@ 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<..> 语法,我们可以按照错误提示修复这个问题,如下所示:

#![allow(unused)]
fn main() {
fn f(x: &()) -> impl Sized + use<'_> { x }
}

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

#![allow(unused)]
fn main() {
trait Captures<T: ?Sized> {}
impl<T: ?Sized, U: ?Sized> Captures<T> for U {}
fn f(x: &()) -> impl Sized + Captures<&'_ ()> { x }
}

这被称为 "the Captures trick",它有点复杂和微妙。现在不再需要了。 还有一种不太正确但更方便的修复方法,通常称为 "the outlives trick"。编译器甚至以前建议这样做。这个技巧看起来像这样:

#![allow(unused)]
fn main() {
fn f(x: &()) -> impl Sized + '_ { x }
}

在这个简单的情况下,这个技巧在细微的原因上与 + use<'_> 完全等价,原因在 RFC 3498 中有解释。然而,在实际情况下,这会过度约束返回的不透明类型的边界。比如如下代码里:

#![allow(unused)]
fn main() {
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<..> 语法目前不能出现在特征或特征实现中,并且必须列出所有在作用域内的泛型类型和常量参数。我们希望随着时间的推移解除这些限制。

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

有关 use<..> 语法、捕获以及这如何应用于 Rust 2024 的更多详细信息,请参阅版本指南中的 "RPIT lifetime capture rules" 章节。有关整体方向的详细信息,请参阅我们最近的博客文章 "Changes to impl Trait in Rust 2024"

创建原始指针的原生语法

不安全代码有时必须处理可能悬空、未对齐或不指向有效数据的指针。这种情况常见于 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() };
}

原生语法更加清晰地将表达式解释为位置表达式(place expressions)。它还避免了在提到创建指针的操作时使用“取地址”的术语。指针不仅仅是地址,因此 Rust 正在摆脱诸如“取地址”之类的强化了指针和地址之间的错误等价关系术语。

unsafe extern 里的 safe 部分

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

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

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

#![allow(unused)]
fn main() {
unsafe extern {  
    pub safe static TAU: f64;
    pub safe fn sqrt(x: f64) -> f64;
    pub unsafe fn strlen(p: *const u8) -> usize;
}
}

这样做的一个好处是,unsafe extern 块中的项可以被标记为安全的调用。在上面的例子中,我们可以在不使用 unsafe 的情况下调用 sqrt 或读取 TAU。没有标记为 safeunsafe 的项会被保守地认为是 unsafe

在未来的版本中,我们将通过 lint 鼓励使用 unsafe extern。从 Rust 2024开始,使用 unsafe extern 将是必需的。

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

不安全的属性

一些 Rust 属性,例如no_mangle,可以在没有 unsafe 块的情况下导致未定义行为。如果是常规代码,我们会要求它们放在 unsafe {}块中,但到目前为止,属性还没有类似的语法。为了反映这些属性可以破坏 Rust 的安全保证,它们现在被认为是“不安全”的,应该写成如下形式:

#![allow(unused)]
fn main() {
#[unsafe(no_mangle)]
pub fn my_global_function() { }
}

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

这会影响以下属性:

  • no_mangle
  • link_section
  • export_name

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

省略模式匹配中的空类型

现在可以省略通过值匹配空(即无法实例化的)类型的模式:

#![allow(unused)]
fn main() {
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] 属性的结构体和枚举。它在与 ! 类型结合使用时也特别有用,尽管该类型目前仍不稳定。

仍然有一些情况下必须编写空模式。由于未初始化值和 unsafe 代码的原因,如果通过引用、指针或联合字段访问空类型,则不允许省略模式:

#![allow(unused)]
fn main() {
pub fn unwrap_ref_without_panic<T>(x: &Result<T, Infallible>) -> &T {
    match x {
        Ok(x) => x,
        // 由于引用,此分支不能省略
        Err(infallible) => match *infallible {},
    }
}
}

为了避免干扰希望支持多个 Rust 版本的 crate,尽管可以删除,但带有空模式的 match 分支尚未报告为“不可达代码”警告。

浮点数 NaN 语义与 const

对浮点数值(类型为 f32f64)的操作以其微妙性而闻名。原因之一是存在“NaN 值”:这是“不是一个数字(not a number)”的缩写,用于表示例如 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() 这样的操作在编译时和运行时可能会产生不同的结果;这不是一个错误,代码不能依赖 const fn 总是产生完全相同的结果。

常量作为汇编立即数

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

#![allow(unused)]
fn main() {
const WRITE_SYSCALL: c_int = 0x01; // 系统调用 1 是 `write`
const STDOUT_HANDLE: c_int = 0x01; // `stdout` 的文件句柄是 1
const MSG: &str = "Hello, world!\n";

let written: usize;

// 签名: `ssize_t write(int fd, const void buf[], size_t count)`
unsafe {
    core::arch::asm!(
        "mov rax, {SYSCALL} // rax 保存系统调用号",
        "mov rdi, {OUTPUT}  // rdi 是 `fd` (第一个参数)",
        "mov rdx, {LEN}     // rdx 是 `count` (第三个参数)",
        "syscall            // 调用系统调用",
        "mov {written}, rax // 保存返回值",
        SYSCALL = const WRITE_SYSCALL,
        OUTPUT = const STDOUT_HANDLE,
        LEN = const MSG.len(),
        in("rsi") MSG.as_ptr(), // rsi 是 `buf *` (第二个参数)
        written = out(reg) written,
    );
}

assert_eq!(written, MSG.len());
}

输出:

Hello, world!

Playground 链接.

在上面的代码中,LEN = const MSG.len() 这样的语句将格式说明符 LEN 填充为一个立即数,其值为 MSG.len()。这可以在生成的汇编代码中看到(值为 14):

lea     rsi, [rip + .L__unnamed_3]
mov     rax, 1    # rax 保存系统调用号
mov     rdi, 1    # rdi 是 `fd` (第一个参数)
mov     rdx, 14   # rdx 是 `count` (第三个参数)
syscall # 调用系统调用
mov     rax, rax  # 保存返回值

更多详情请参见 参考文档

安全地访问不安全的 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 提示,但它们现在只在旧版本中才有用。如果你想支持多个版本的 Rust,可以在这些不安全的块上添加 #[allow(unused_unsafe)] 注解,如下例所示:

 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 版本预计会将此功能推广到其他在此位置安全的表达式,而不仅仅是静态变量。

Others

其它更新细节,和稳定的 API 列表,参考原Blog