Comprehensive Rust 深入 unsafe(一):定义 unsafe 函数——以 ptr_to_ref 为例剖析责任转移与安全前置条件
【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust
本篇技术指南聚焦 Google Android 团队维护的 Rust 教学仓库 comprehensive-rust 中《Unsafe Deep Dive》热身环节的核心一课——如何定义 unsafe 函数(对应课程文档 warm-up/unsafe-fn.md)。课程通过一个将裸指针转换为引用的ptr_to_ref示例,揭示了一个反直觉的事实:看起来完全安全的代码,实际上必须借助unsafe块才能成立。读完本文,你将掌握 unsafe 函数与普通函数的分界、unsafe关键字的两重角色、API 设计者如何通过"责任转移"把内存安全前置条件交给调用方,以及如何在标准库(如ptr::as_mut)中定位并解读这些前置条件。
热身课:四种 unsafe 语法形态
在进入ptr_to_ref之前,先建立全局坐标系。本仓库将 unsafe 语法拆成四个独立热身示例(见 warm-up.md):
| 语法形态 | 语法示例 | 对应课程文档 |
|---|---|---|
| unsafe 块 | unsafe { ... } | warm-up/unsafe-block.md |
| unsafe 函数 | unsafe fn | warm-up/unsafe-fn.md |
| unsafe trait 的实现 | unsafe impl { ... } | warm-up/unsafe-impl.md |
| unsafe trait 的定义 | unsafe trait | warm-up/unsafe-trait.md |
其中,"定义 unsafe 函数"这一课是全套 unsafe 教学的起点:它回答了一个根本性问题——什么样的 API 必须用unsafe fn声明,声明之后责任又转移给了谁。
核心示例:ptr_to_ref,一段"看似安全"的代码
课程给出的示例代码如下(来源:unsafe-fn.md):
/// Convert a nullable pointer to a reference. /// /// Returns `None` when `p` is null, otherwise wraps `val` in `Some`. fn ptr_to_ref<'a, T>(ptr: *mut T) -> Option<&'a mut T> { if ptr.is_null() { None } else { // SAFETY: `ptr` is non-null unsafe { Some(&mut *ptr) } } }初看之下,这段代码似乎完全可以脱离unsafe编写:if ptr.is_null()已经对空指针做了分支处理,非空分支似乎"顺理成章"地返回引用。然而课程明确指出:
"This looks as though it's safe code, however it actually requires an unsafe block."(这段代码看起来像是安全代码,但实际上需要一个 unsafe 块。)
关键点在于unsafe { Some(&mut *ptr) }中的解引用操作*ptr——将裸指针解引用并转换为可变引用,是编译器无法替你验证安全的操作。课程建议教师在讲解时高亮这一行解引用,让学员直观看到"问题出在哪里"。
为什么"非空指针"依然不够:指针并不都能变成引用
课程强调了一个反直觉的结论:
"Callers must ensure that the
ptris null, or that it may be converted to a reference."(调用方必须确保指针为 null,或者它可以被转换为引用。) "It may be counter-intuitive, but many pointers cannot be converted to references."(可能违反直觉,但很多指针并不能被转换为引用。)
为什么?课程给出的解释是:
"Among other issues, a pointer could be created that points to some arbitrary bits rather than a valid value."(指针可能指向任意比特,而不是一个合法的值。)
也就是说,*mut T只是一串内存地址,它既可以指向一个合法构造的T实例,也可能指向:
- 未初始化的内存(garbage bits);
- 已经释放/失效的对象;
- 不满足对齐要求或生命周期约束的地址。
而 Rust 的引用有严格的合法性约束(对齐、有效生命周期、别名规则等)。从裸指针到引用的转换,隐含了"该指针必须 dereferenceable(可解引用)且满足 Rust 别名规则"等前置条件——这些条件无法通过ptr.is_null()这类运行时检查完全保障。ptr_to_ref的函数体只验证了"非空"这一条,其余条件必须由调用方保证,这正是函数必须声明为 unsafe 的根本原因。
更深入的指针转引用规则,本仓库在 safety-preconditions/determining.md 中有完整讲解(详见下文"标准库中的对应物"一节)。
API 设计者的两条路:自行防御,还是转移责任
课程指出,面对"调用方可能传入非法指针"这一现实,API 设计者有两条路可走:
- 第一条路:自行承担防御职责——在函数内部穷尽所有非法输入的检查;
- 第二条路:用
unsafe关键字把责任转移给调用方。
为什么第一条路在泛型场景下"走不通"
课程对第一条路给出了明确的否定理由:
"The first path is a difficult one. We're accepting a generic type T, which is all possible types that implement Sized. That's a lot of types!"(第一条路很困难。我们接受泛型 T,它涵盖所有实现了 Sized 的类型,那可是一大堆类型!)
这里的论证非常精妙:fn ptr_to_ref<'a, T>(ptr: *mut T)中的T: Sized(默认约束)意味着该函数要为所有尺寸已知的类型负责。对每一种T,"什么样的指针才是合法指针"的判断标准都不同——例如T = u8与T = 某个含引用字段的结构体,其合法性前置条件天差地别。想在泛型函数内部穷举并防御所有可能的非法输入,本质上不可行。
第二条路:unsafe 即责任契约
因此课程得出结论:
"Therefore, the second path makes more sense."(因此第二条路更有意义。)
即:把ptr_to_ref声明为unsafe fn,并在文档中写明调用方必须满足的前置条件(指针为 null,或可安全转换为引用)。unsafe关键字在这里是一份显式的责任契约:函数作者声明"我无法替你验证一切,请你满足这些前置条件",调用方则承诺"我已阅读并满足这些条件"。
这一结论与本仓库 introduction/responsibility-shift.md 中"unsafe 关键字转移责任"的表格完全呼应:
| 场景 | 内存是否安全? | 内存安全责任方 |
|---|---|---|
| Safe Rust | 是 | 编译器 |
| Unsafe Rust | 是 | 程序员 |
课程原话:"Theunsafekeyword shifts responsibility for maintaining memory safety from the compiler to programmers. It signals that there are preconditions that must be satisfied."(unsafe关键字将维护内存安全的职责从编译器转移到程序员身上,它标志着存在必须满足的前置条件。)后文中,本课程统一使用术语safety preconditions(安全前置条件)来描述这种需要调用方保证的局面。
为什么"定义"unsafe 函数与"使用"unsafe 块是两回事
ptr_to_ref示例恰好同时演示了unsafe关键字的两种角色。本仓库在 introduction/two-roles.md 中将其总结为:
- 创建(Creating)带安全考量点的 API:
unsafe fn get_unchecked(&self) { ... }、unsafe trait Send {}。在这个角色下,你在告诉 API 的使用者:"请小心,你需要注意我的安全要求。"同时,API 作者有义务把需要何种小心(即安全要求)写进文档——"Unsafe APIs are not complete without documentation about safety requirements"(没有安全需求文档的 unsafe API 是不完整的)。 - 使用(Using)带安全考量点的 API:
unsafe { *ptr }调用内置 unsafe 运算符、unsafe { x.get_unchecked() }调用 unsafe 函数、unsafe impl Send for Counter {}实现 unsafe trait。当unsafe紧邻花括号出现时,它表示作者已经小心过——"the author has been careful. They have verified that the code is safe"(作者已经仔细验证过代码是安全的)。
对应到ptr_to_ref:
- 定义层面:该函数虽然没有声明为
unsafe fn(课程刻意如此设计以制造讨论点),但其函数体内部使用 unsafe 块调用了"解引用裸指针"这一编译器已知的 unsafe 操作; - 使用层面:
unsafe { Some(&mut *ptr) }块内的代码,是作者对"ptr已验证非空、调用方已保证指针合法"这一结论的担保声明。
值得注意的是,课程在此处留了一个值得深挖的张力点:ptr_to_ref以普通fn对外暴露,却在其内部执行 unsafe 操作。这引出一个更规范的写法问题——如果调用方仍可能传入非法指针,那么该函数应当整体声明为unsafe fn,把前置条件显式暴露给调用方,这正是课程"责任转移"哲学的直接推论。(关于 unsafe 函数的规范声明方式与调用约束,可继续学习本仓库的 unsafe-rust/unsafe-functions.md 与 unsafe-rust/unsafe.md。)
补充讲解:ptr_to_ref 在标准库中的真实对应物——ptr::as_mut
课程在"时间允许的附加内容"中给出了一条极具价值的线索:
"For example, the
ptr_to_reffunction on this slide actually exists in the standard library as theas_mutmethod on pointers."(本页的ptr_to_ref函数其实在标准库中就存在,即指针上的as_mut方法。)
也就是说,你不需要手写ptr_to_ref——标准库为裸指针提供了as_mut(以及对应的as_ref)方法,其语义与示例完全一致:指针为 null 时返回None,否则返回Some(&mut *ptr)。这为学员提供了一个"照着标准库学 unsafe"的范本:标准库中大量方法(尤其是std::ptr上的方法)都以极规范的 Safety 文档公开了各自的前置条件。
本仓库在 safety-preconditions/determining.md 中进一步演示了如何查找as_mut的前置条件。课程给出了一份"去哪里查前置条件"的清单:
- 函数 API 文档,尤其是其中的Safety 段落;
- 源码及其内部的 SAFETY 注释;
- 模块级文档;
- Rust Reference(参考手册)。
其中as_mut的官方 Safety 说明原文为:
SafetyWhen calling this method, you have to ensure that either the pointer is null or the pointer is convertible to a reference. (调用此方法时,你必须确保指针为 null,或者指针可被转换为引用。)
课程还提示,点击文档中 "convertible to a reference" 的超链接,可以追踪"指针到引用转换"的详细规则(即指针必须 dereferenceable)。以 Rust 1.90.0 版本为例,文档甚至指出"You must enforce Rust's aliasing rules. The exact aliasing rules are not decided yet"(你必须遵守 Rust 的别名规则,而确切的别名规则尚未最终定案)——这说明指针转引用的部分规则仍在演进,更凸显了以文档为准、以unsafe显式声明责任的重要性。
安全工作流:理解前置条件、验证并留下 SAFETY 注释
理解了责任转移与前置条件之后,本仓库 introduction/impact-on-workflow.md 给出了落地到日常开发的建议:
写代码时:
- 验证你理解每一个
unsafe函数/trait 的前置条件; - 检查这些前置条件是否确实被满足;
- 把你的推理过程写成safety comments(安全注释)。
这正是示例代码中// SAFETY: \ptr` is non-null注释的由来——它不是可有可无的装饰,而是"作者已经验证过"的书面证据。同样的做法也出现在本仓库的配套热身课 [warm-up/unsafe-block.md](https://link.gitcode.com/i/656ebeb407863fec1f460d35366572ad) 中:那里使用vec![0, 1, 2, 3, 4]配合get_unchecked(i)(i = len/2),演示了先触发编译器报错(unsafe 调用必须包在 unsafe 块内),再补上unsafe { ... }与// SAFETY: `i` must be within 0..numbers.len()` 注释的完整流程。
强化代码评审:
- 评审链路建议为:自评 → 同行评审 →(必要时)unsafe Rust 专家评审;
- 遇到拿不准的情况,升级给"对你的代码与推理感到放心"的人。
课程对此的解释很务实:"There are only a few unsafe Rust experts, and they are very busy, so we need to optimally use their time."(unsafe Rust 专家很少且非常忙碌,我们需要最优地利用他们的时间。)简单 unsafe 代码由作者与主评审自行把关,疑难场景才请专家介入。
小结:从 ptr_to_ref 学到的 unsafe 函数设计要点
| 要点 | 结论 | 出处 |
|---|---|---|
| 判据 | 若函数内部依赖调用方保证内存安全前置条件,应声明为unsafe fn | warm-up/unsafe-fn.md |
| 反直觉点 | 非空指针 ≠ 可转换为引用,指针可能指向任意比特 | 同上 |
| 两条设计路径 | 泛型场景下穷举防御不可行,责任转移更合理 | 同上 |
| unsafe 两角色 | 创建(unsafe fn/unsafe trait)vs 使用(unsafe 块/unsafe impl) | introduction/two-roles.md |
| 责任归属 | Safe Rust → 编译器;Unsafe Rust → 程序员 | introduction/responsibility-shift.md |
| 文档义务 | 无安全需求文档的 unsafe API 是不完整的 | introduction/two-roles.md |
| 实战参照 | 标准库ptr::as_mut即ptr_to_ref的工业级实现 | safety-preconditions/determining.md |
| 工作流 | 理解前置条件 → 验证满足 → 写 SAFETY 注释 → 分级评审 | introduction/impact-on-workflow.md |
如果你希望在课程中继续深入,推荐按以下路径推进:
- 热身环节其余三个示例:unsafe block、unsafe impl、unsafe trait;
- 概念铺垫:Why the unsafe keyword exists 与 Defining Unsafe Rust(Unsafe Rust 是 Safe Rust 的超集,解引用裸指针是标准库实现
Vec、Box的根基); - 前置条件的系统化学习:safety-preconditions 系列,包括如何从文档与源码中确定前置条件、常见的语义前置条件等。
一句话总结:unsafe函数是 Rust 中"把编译器无法验证的合法性义务显式交给调用方"的机制——ptr_to_ref这个看似平凡的例子,浓缩了 unsafe Rust 设计的全部核心思想:前置条件、责任转移、文档义务与 SAFETY 注释。
【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考