Rust 新版解读 | 1.78 | 诊断属性宏

Rust 1.78 官方 release doc: Announcing Rust 1.78.0 | Rust Blog

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

$ rustup update stable

诊断属性宏

Rust 现在支持 #[diagnostic] 属性命名空间,用于影响编译器错误消息。这些被视为提示,编译器不需要使用它们,也不会因为提供了编译器不认识的诊断而报错。这种灵活性允许源代码提供诊断,即使不是所有编译器都支持,无论是不同版本还是完全不同的实现。

随着这个命名空间的出现,第一个支持的属性 #[diagnostic::on_unimplemented] 也随之而来,可以放在一个 trait 上,用于自定义当需要但未在类型上实现该 trait 时的消息。考虑下面来自稳定 PR 里的示例:

#[diagnostic::on_unimplemented(
    message = "My Message for `ImportantTrait<{A}>` is not implemented for `{Self}`",
    label = "My Label",
    note = "Note 1",
    note = "Note 2"
)]
trait ImportantTrait<A> {}

fn use_my_trait(_: impl ImportantTrait<i32>) {}

fn main() {
    use_my_trait(String::new());
}

此前,编译器会给出一个内置错误,如下:

error[E0277]: the trait bound `String: ImportantTrait<i32>` is not satisfied
  --> src/main.rs:12:18
   |
12 |     use_my_trait(String::new());
   |     ------------ ^^^^^^^^^^^^^ the trait `ImportantTrait<i32>` is not implemented for `String`
   |     |
   |     required by a bound introduced by this call
   |

现在,使用 #[diagnostic::on_unimplemented],自定义消息填充主要错误行,自定义标签放在源输出上。原始标签仍然写在帮助输出中,任何自定义注释也会被写入。 (这些细节未来可能会发生变化。)

error[E0277]: My Message for `ImportantTrait<i32>` is not implemented for `String`
  --> src/main.rs:12:18
   |
12 |     use_my_trait(String::new());
   |     ------------ ^^^^^^^^^^^^^ My Label
   |     |
   |     required by a bound introduced by this call
   |
   = help: the trait `ImportantTrait<i32>` is not implemented for `String`
   = note: Note 1
   = note: Note 2

对于 trait 作者来说,如果你能提供更好的提示,而不是仅仅给出缺失部分,这种诊断就更有用。例如,这是标准库中的一个示例:

#![allow(unused)]
fn main() {
#[diagnostic::on_unimplemented(
    message = "the size for values of type `{Self}` cannot be known at compilation time",
    label = "doesn't have a size known at compile-time"
)]
pub trait Sized {}
}

更多信息,请参考诊断工具属性命名空间的参考部分。

不安全前提断言

Rust 标准库有许多用于不安全函数前提的断言,但历史上它们只在标准库的 #[cfg(debug_assertions)] 构建中启用,以避免影响发布性能。然而,由于标准库通常以发布模式编译和分发,大多数 Rust 开发者根本没有执行这些检查。

现在,这些断言的条件被延迟到代码生成,因此它们将根据用户自己对调试断言的设置进行检查(在调试和测试构建中默认启用)。这个变化有助于用户捕获他们代码中的未定义行为。

例如,slice::from_raw_parts 需要一个对齐的非空指针。下面故意错位指针的使用有未定义行为,虽然如果你运气不好,它可能在过去看起来“工作”,但调试断言现在可以捕获它:

fn main() {
    let slice: &[u8] = &[1, 2, 3, 4, 5];
    let ptr = slice.as_ptr();

    // 创建一个 `ptr` 的偏移量,它总是比 `u16` 的正确对齐少一个
    let i = usize::from(ptr as usize & 1 == 0);
    
    let slice16: &[u16] = unsafe { std::slice::from_raw_parts(ptr.add(i).cast::<u16>(), 2) };
    dbg!(slice16);
}

在调试构建中,这将导致一个 panic:

thread 'main' panicked at library/core/src/panicking.rs:220:5:
unsafe precondition(s) violated: slice::from_raw_parts requires the pointer to be aligned and non-null, and the total size of the slice not to exceed `isize::MAX`
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
thread caused non-unwinding panic. aborting.

确定性重新对齐

标准库有一些函数可以改变指针和切片的对齐方式,但以前它们有一些注意事项,使它们在实践中难以依赖,如果你严格遵循它们的文档。这些注意事项主要是为了保护对 const 的判断,但它们只对非 const 使用稳定。现在它们承诺根据实际输入具有一致的运行时行为。

  • pointer::align_offset 计算改变指针到给定对齐方式所需的偏移量。如果不可能,它将返回 usize::MAX,但以前允许它始终返回 usize::MAX,现在这种行为被移除。

  • slice::align_toslice::align_to_mut 都将切片转换为对齐的中间切片和剩余的不对齐头和尾切片。这些方法现在承诺返回最大可能的中间部分,而不允许实现返回不那么优化的东西,比如将所有东西作为头切片返回。

Others

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