LWN:Rust 里安全的结构体表达方案!

关注了就能看到更多这么棒的文章哦~

Toward safe transmutation in Rust

By Daroc Alden
October 23, 2024
RustConf 2024
Gemini-1.5-flash translation
https://lwn.net/Articles/994334/

目前,Rust 缺乏高效且安全的将字节数组转换为对应结构体的方法。今年的 RustConf 大会上,Jack Wrenn 在他的演讲 "Safety Goggles for Alchemists" 中谈到了这个问题。目标是能够以更安全的方式进行「transmute」(Rust 中对这种转换的称呼)—— 将值转换为任意用户定义的类型。Wrenn 解释了该项目采用的方法,并讨论了未来需要完成的工作以使其稳定。

基本方案是将现有的不安全 std::mem::transmute() 函数(该函数指示编译器将内存的一部分重新解释为不同的类型,但需要程序员确保这样做是合理的)转变为安全的版本,并能自行检查必要的约束条件。Wrenn 演讲的第一部分重点介绍了这些约束条件是什么以及如何检查它们。

cbf4cdfe9a773158a56876fcc5c8c09b.png

首先要考虑的是位有效性—— 输入类型能够产生的每种位模式是否也对输出类型有效。例如,将 bool 转换为 u8 是有效的,因为每个布尔值都存储为一个字节,因此也是有效的 u8=。另一方面,将 =u8 转换为 bool 是无效的,因为一些 u8 的值不对应于 bool (例如,17)。接下来要考虑的是对齐。某些类型必须与内存中的特定边界对齐。例如, u16 值在大多数平台上必须与偶数地址对齐。从一种类型转换为另一种类型只有在该类型的存储与目标类型的值的对齐边界足够大的情况下才是有效的。

任何语言中实现类型转换的代码都需要考虑位有效性和对齐,但还有两个安全类型转换的独特要求,它们是 Rust 独有的:生命周期和由构造函数维护的安全约束。这两者都与 Rust 使用类型系统验证程序员指定的约束条件的方式有关。如果类型转换会破坏 Rust 的生命周期跟踪,那么它是无效的。但是,如果它允许某人构造一个没有公开构造函数的类型,那么它也可能无效。例如,许多 Rust API 会分发在析构时执行某些操作的保护对象。如果程序员能够将字节数组转换为某个互斥锁的 MutexGuard 而无需锁定它,则可能会导致严重问题。因此,类型转换也不应该用于创建通过智能构造函数来维护安全要求的类型。

尽管如此,如果程序员能够确保满足这四个条件,类型转换还是非常有用的。Wrenn 以解析 UDP 数据包为例。在传统的解析器中,程序员必须至少复制 UDP 标头中的所有数据一次,才能将其从传入缓冲区移动到结构体中。但 UDP 标头被设计成可以简单地直接解释为一个结构体,只要其字段具有正确的尺寸。这可以让程序在没有任何复制的情况下解析数据包。

因此,拥有安全的类型转换将非常有用。这促使 Rust 社区创建了多个 crate,它们围绕类型转换提供了安全的抽象。Wrenn 强调了两个:bytemuck 和 zerocopy。他是 zerocopy 的共同维护者,因此他选择这个 crate 来“挑剔”。

他解释说,这两个 crate 都是通过添加一个标记 trait 来工作的—— 一个没有方法的 trait,它只存在是为了让程序员能够编写类型约束,这些约束指定一个类型需要实现该 trait 才能在某个函数中使用。该 trait 的实现是不安全的,因此实现它本质上是对 zerocopy 的承诺,即程序员已经阅读了相关文档并确保该类型满足库的要求。然后,库本身可以包含对基本类型的实现,以及一个宏来为可以安全地实现标记 trait 的结构体实现该标记 trait。这种方法是有效的。他表示,Google 在 Fuchsia 操作系统的网络堆栈中使用了这种方法。

但 Wrenn 警告说,zerocopy 有一个“肮脏的秘密”:它依赖于近 14,000 行细致的不安全代码。更糟糕的是,大部分代码都重复了编译器出于其他原因必须执行的分析。如果这种能力内置到编译器中会更有用。

“Project Safe Transmute”

Wrenn 说,所有这些都是促使创建 "Project Safe Transmute" 的原因。该项目试图为 Rust 编译器提供对安全类型转换的原生支持。

Wrenn 解释说,该项目基于特定的“类型炼金术理论”。这个想法是跟踪一种类型的全部可能值是否也是另一种类型的可能值。例如,=NonZeroU8= 可以转换为 u8,无需检查,但反之则不行。但自动确定这种关系比最初看起来要棘手。通过基于可能值集合进行推理,以朴素的方式进行分析很快就会变得效率低下。Wrenn 说,相反,编译器将类型建模为 有限状态机。类型中的每个字段或填充部分都成为一个状态,边代表有效值。因此,所有值都由机器中的路径表示,可以使用相对简单的算法进行处理,但当类型变得更加复杂时,表示的规模不会爆炸性增长。

有了这种理论,在编译器中实现这种分析就变得切实可行了。因此,Wrenn 和他的合作者实现了它,最终得到了以下 trait,该 trait 由编译器自动为任何两个兼容类型动态实现:

unsafe trait TransmuteFrom<Src: ?Sized> {
    fn transmute(src: Src) -> Dst
    where
        Src: Sized,
        Self: Sized;
}

由于这项工作被集成到编译器中,尝试转换两个不兼容的类型将给出自定义错误消息,解释原因。编译器检查 Wrenn 之前描述的所有四个要求—— 这恰好是下一个问题的来源。编译器如何知道用户定义的类型是否具有由构造函数检查的安全要求?它不知道,因此它必须保守地假设用户定义的类型不能成为类型转换的目标(尽管它们仍然可以成为输入)。

但这“并没有那么有用”。将事物转换为用户定义的类型是 Wrenn 讨论的用例所必需的。事实证明,人们想要的通常不是安全的类型转换,而是更安全的类型转换。因此,负责类型转换的人员在 TransmuteFrom trait 中添加了一个额外的泛型参数,程序员可以使用它来向编译器保证一个或多个安全要求得到满足,即使编译器无法证明这一点。这些参数是 Assume::VALIDITY 用于位有效性,=Assume::ALIGNMENT= 用于对齐,=Assume::LIFETIMES= 用于生命周期,以及 Assume::SAFETY 用于用户安全约束。现在,可以通过向操作提供 Assume::SAFETY 参数来对用户类型进行转换:

#[repr(transparent)]
pub struct Even {
    // 编译器不知道以下内容,
    // 但我们的代码出于某种原因依赖于它:
    // SAFETY: 始终是偶数!
    n: u8
}

fn u8_to_even(src: u8) -> Even {
    assert!(src % 2 == 0)
    unsafe { TransmuteFrom::<_, Assume::SAFETY>::transmute(src) }
}

可能看起来使用 unsafe 进行类型转换似乎意味着缺乏进展。但是,这种设计有一个优点,即程序员只需要断言编译器无法证明的特定约束条件的安全性—— 上面的代码仍然使用编译时检查来检查位有效性、对齐和生命周期问题。因此,这项工作(可在 nightly 上进行测试)并没有使类型转换完全安全,但它确实提供了“有效的安全护目镜”,以确保尽可能由编译器进行检查,因此程序员只需要检查那些编译器确实无法确定的内容。

未来展望

Wrenn 最后总结了完善该功能所需的未来工作:支持动态大小的类型,添加一个 API 用于可失败的类型转换,优化编译器中位有效性检查的实现,改进类型布局的可移植性,最后是使这项工作稳定。他希望 TransmuteFrom 可能会在 2025 年有一个 RFC 用于稳定,但他表示它需要测试和反馈才能做到,并呼吁听众提供这些测试。用户是否会发现这个 API 比现有的 crate 更好还有待观察,但似乎很明显,类型转换太有用,不能以某种方式作为 Rust 本身的一部分进行支持。

全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~

27335553e68d4cf19cd7e7e1d50ed9a8.jpeg

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值