news 2026/7/10 2:35:16

Rust unsafe 入门:什么时候必须用 unsafe,以及怎么把它包安全

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Rust unsafe 入门:什么时候必须用 unsafe,以及怎么把它包安全

Rust unsafe 入门:什么时候必须用 unsafe,以及怎么把它包安全


一、unsafe 到底"关掉"了什么检查

很多人以为unsafe关键字一写,Rust 编译器就彻底撒手了。其实不是。它只放开五件事,其余检查照旧:

mindmap root((unsafe 放开的五件事)) 解引用裸指针 原始指针 *const T / *mut T 不受借用规则约束 调用 unsafe 函数 FFI 外部函数 标注了 unsafe 的库函数 访问可变静态变量 static mut 数据竞争风险 实现 unsafe trait Send / Sync 等标记 trait 告诉编译器"我保证线程安全" 访问 union 字段 联合体的字段读写 C 互操作常用

重点来了:即使在一个unsafe块里面,Rust 仍然会检查:

  • 变量的所有权规则(你仍然不能把一个值移动两次)
  • 生命周期标注是否一致
  • 模式匹配是否穷尽
  • 类型是否匹配

所以我现在的理解是:unsafe不是"免死金牌",它是一个契约边界——编译器的"我保证不了,这部分你来"。你写unsafe的意思就是:"编译器你不用管了,出了事我负责"。


二、什么时候必须用 unsafe——四个绕不开的场景

作为自学的,我一开始特别抗拒 unsafe。但写了一些项目后发现,以下场景你真的绕不过去:

场景 1:调用 C 库(FFI)

这是最没得选的场景。你只要调用任何一个 C 函数,都得进 unsafe。

// ========== FFI 调用必须用 unsafe ========== // 假设有这个 C 库函数: int read_file(const char* path, char* buf, int size); extern "C" { /// C 函数声明 —— Rust 编译器无法验证它的安全性 fn read_file(path: *const i8, buf: *mut u8, size: i32) -> i32; } /// 安全的包装函数 pub fn safe_read_file(path: &str) -> Result<String, String> { // 1️⃣ 准备好 C 风格的字符串 let c_path = std::ffi::CString::new(path) .map_err(|e| format!("路径包含空字节: {e}"))?; // 2️⃣ 分配缓冲区 let mut buf = vec![0u8; 4096]; // 3️⃣ unsafe 块 —— 这是我们保证安全的边界 let bytes_read = unsafe { // 解引用裸指针,调用 C 函数,编译器不管 read_file(c_path.as_ptr(), buf.as_mut_ptr(), buf.len() as i32) }; // 4️⃣ 检查返回值(C 函数的返回值需要手动检查) if bytes_read < 0 { return Err(format!("读取文件失败,错误码: {bytes_read}")); } // 5️⃣ 转换回 Rust 的 String buf.truncate(bytes_read as usize); String::from_utf8(buf) .map_err(|e| format!("文件内容不是有效的 UTF-8: {e}")) } // ========== 关键点 ========== // 调用 safe_read_file 的函数不需要 unsafe, // 因为所有不安全操作都已经被"包"在内部了。

场景 2:高性能数据结构(自己实现链表/无锁队列)

Rust 标准库的LinkedList是安全的,但如果你要实现一个侵入式链表或者无锁数据结构,不可避免地要操作裸指针。

// ========== 自实现侵入式链表节点 ========== use std::ptr::NonNull; /// 双向链表节点 struct Node<T> { value: T, prev: Option<NonNull<Node<T>>>, // 用 NonNull 而不是裸指针,语义更清晰 next: Option<NonNull<Node<T>>>, } impl<T> Node<T> { fn new(value: T) -> Self { Node { value, prev: None, next: None, } } /// 在当前节点后插入新节点 fn insert_after(&mut self, mut new_node: Box<Node<T>>) { // 安全前提:调用者保证 self 在链表中的位置是确定的 unsafe { // 1️⃣ 设置新节点的前后指针 new_node.prev = Some(NonNull::new_unchecked(self as *mut Node<T>)); new_node.next = self.next; // 2️⃣ 如果有后继节点,更新它的 prev 指针 if let Some(mut next) = self.next { next.as_mut().prev = Some(NonNull::new_unchecked(new_node.as_mut())); } // 3️⃣ 更新当前节点的 next let new_ptr = NonNull::new_unchecked(Box::into_raw(new_node)); self.next = Some(new_ptr); } } } // ========== 设计要点 ========== // 1. insert_after 本身不是 unsafe —— 调用者不需要写 unsafe 块 // 2. 但内部用 unsafe { } 块明确标记了不安全区域 // 3. NonNull 比 *mut T 更安全,因为它保证非空

场景 3:操作硬件/内存映射 I/O

嵌入式或者操作系统开发中需要直接操作内存地址:

// ========== 内存映射 I/O 的 unsafe 包装 ========== /// GPIO 寄存器地址(假设) const GPIO_BASE: usize = 0x3F20_0000; const GPIO_SET_OFFSET: usize = 0x1C; /// 设置 GPIO 引脚为高电平 —— 安全包装 pub fn gpio_set_high(pin: u8) { assert!(pin <= 53, "引脚号必须在 0-53 之间"); unsafe { // 将固定地址转为可变裸指针 —— unsafe 不可省略 let gpio_set = (GPIO_BASE + GPIO_SET_OFFSET) as *mut u32; // 写入寄存器 gpio_set.write_volatile(1 << pin); } } // 虽然内部有 unsafe,但调用方不用 —— 引脚范围检查在 unsafe 外面做完了

场景 4:突破借用规则(但你不该这么做)

这个场景我放在最后,因为"能做到"不代表"应该做"。绝大多数情况下,如果你觉得需要unsafe来绕开借用检查,应该先反思是不是设计有问题。

// ⚠️ 这是一个"技术上可行但绝大多数场景应该避免"的例子 // 用于说明 unsafe 的能力边界,不代表推荐做法 struct SplitBuffer { data: Vec<u8>, } impl SplitBuffer { /// 同时获取两个不重叠的可变切片 fn split_at_mut(&mut self, mid: usize) -> Option<(&mut [u8], &mut [u8])> { let len = self.data.len(); if mid > len { return None; } let ptr = self.data.as_mut_ptr(); unsafe { // 虽然生成了两个 &mut,但它们指向不重叠的内存区域 —— 不会数据竞争 Some(( std::slice::from_raw_parts_mut(ptr, mid), std::slice::from_raw_parts_mut(ptr.add(mid), len - mid), )) } } }

注意:标准库的split_at_mut已经提供了这个功能,而且是用 unsafe 实现的。你的代码应该用标准库版本,而不是自己写。


三、怎么把 unsafe 包安全——四条铁律

flowchart TD A[需要 unsafe 操作] --> B{能否缩小 unsafe 范围?} B -->|能| C[unsafe 块尽量小] B -->|不能| D[用注释写清楚安全前提] C --> E{调用方需要写 unsafe 吗?} E -->|需要| F[标记函数为 unsafe] E -->|不需要| G[函数不加 unsafe 标记] D --> G F --> H[文档写清调用者要保证什么] G --> I[内部注释写清为什么安全] H --> J{能证明安全吗?} I --> J J -->|能| K[✅ 通过代码审查] J -->|不能| L[❌ 重新设计] style K fill:#6bcb77,stroke:#333 style L fill:#ff6b6b,stroke:#333

把 unsafe 包安全的四条铁律:

铁律 1:unsafe 块越小越好

// ❌ 不好 —— 整个函数都是 unsafe,分不清哪里不安全 unsafe fn process_data(data: *const u8, len: usize) -> Vec<u8> { let slice = std::slice::from_raw_parts(data, len); let mut result = Vec::with_capacity(len); result.extend_from_slice(slice); result } // ✅ 好 —— unsafe 只包住真正不安全的那一行 fn process_data(data: *const u8, len: usize) -> Vec<u8> { // 参数校验在 unsafe 外面 assert!(!data.is_null(), "数据指针不能为空"); assert!(len > 0, "长度必须大于 0"); let slice = unsafe { // 只有这一行是不安全的 —— 从裸指针创建切片 std::slice::from_raw_parts(data, len) }; // 其余操作都是安全的 let mut result = Vec::with_capacity(len); result.extend_from_slice(slice); // 安全:slice 生命周期正确 result }

铁律 2:安全前提必须用注释或断言"证明"

/// 从原始字节创建字符串。 /// /// # Safety(内部保证的安全前提) /// /// - 调用者必须保证 `bytes` 是有效的 UTF-8。 /// 本函数会先调用 `from_utf8` 检查,不依赖外部保证。 /// /// # 外部调用 /// /// 本函数 **不是** unsafe,调用者无需 unsafe 块。 pub fn bytes_to_string(bytes: &[u8]) -> Result<String, String> { // 在可能导致 unsound 之前,用安全的检查兜底 String::from_utf8(bytes.to_vec()) .map_err(|e| format!("无效的 UTF-8 编码: {e}")) }

铁律 3:用类型系统把 unsafe"关在笼子里"

use std::marker::PhantomData; /// 一个"被验证过的"原始指针包装类型 /// /// 这个类型的存在本身就意味着:持有它的人已经确认过指针的有效性。 /// 外界无法直接构造,必须通过 `validate` 方法。 pub struct ValidatedPtr<T> { ptr: *const T, _phantom: PhantomData<T>, } impl<T> ValidatedPtr<T> { /// 唯一构造入口 —— 验证指针非空 pub fn validate(ptr: *const T) -> Option<Self> { if ptr.is_null() { None } else { Some(ValidatedPtr { ptr, _phantom: PhantomData, }) } } /// 安全地解引用 —— 因为构造时已验证非空 pub fn as_ref(&self) -> &T { unsafe { &*self.ptr } } } // 任何拿到 ValidatedPtr 的代码都可以安全调用 as_ref(), // 不需要再写 unsafe,因为不变量在构造时已保证。

铁律 4:宁愿拒绝编译也不要写不确定的 unsafe

这是的最痛领悟。有一次我想用 unsafe 绕开一个生命周期问题,搞了两个小时,最后发现换一种数据结构设计根本不需要 unsafe。如果你对 unsafe 块的安全性有一丝不确定,就换个方案

// ❌ 不要这样 —— 为了"性能"写不清晰的 unsafe // ✅ 先用 safe 代码实现,profile 证明是瓶颈后再考虑优化

四、必须用 unsafe 实现 unsafe trait 的场景

这是 Rust 里最常见也最容易被忽略的 unsafe 用法。当你自己实现SendSynctrait 时,相当于你在对编译器说:"这个类型跨线程传输是安全的,我保证"。

use std::cell::UnsafeCell; use std::sync::Arc; /// 一个线程安全的计数器实现(类似标准库的 AtomicUsize,这里是教学简化版) struct MyAtomicUsize { // UnsafeCell 是 !Sync 的,所以包含它的结构体默认不是 Sync inner: UnsafeCell<usize>, } // ⚠️ unsafe impl —— 我们手动告诉编译器这个类型是 Sync 的 // 前提:所有对 inner 的访问都通过原子操作或适当的同步机制 unsafe impl Sync for MyAtomicUsize {} impl MyAtomicUsize { pub fn new(val: usize) -> Self { MyAtomicUsize { inner: UnsafeCell::new(val), } } /// 安全地读取值(这里简化了,实际应该用原子操作) pub fn load(&self) -> usize { // unsafe 块很小:只包住了对 UnsafeCell 的访问 unsafe { *self.inner.get() } // 这个 unsafe 是安全的,因为: // 1. 我们通过 &self 获取,没有 &mut 存在 // 2. 这个值只是一个 usize,读取是原子的(在大多数平台上) // 如果这是生产代码,应该用 AtomicUsize } } // 使用示例 —— 可以安全地在多线程间共享 let counter = Arc::new(MyAtomicUsize::new(0)); let c = counter.clone(); std::thread::spawn(move || { println!("计数: {}", c.load()); });

五、总结

这篇文章把 unsafe 的核心梳理了一遍:

  1. unsafe 只放开五件事:裸指针解引用、调用 unsafe 函数、访问可变静态变量、实现 unsafe trait、访问 union 字段。其余 Rust 安全检查照常工作。

  2. 四个绕不开的 unsafe 场景:FFI 调用 C 库、高性能自定义数据结构、硬件/内存映射 I/O、极少见的借用规则突破。除此之外,绝大多数场景都不需要 unsafe。

  3. 四条包装铁律:unsafe 块尽量小、安全前提必须可证明、用类型系统把 unsafe 关在笼子里、不确定就别用。

  4. unsafe trait(Send/Sync)是你对编译器的郑重承诺,写之前要想清楚"我拿了什么同步机制来保证"。

作为独立开发者,我对 unsafe 的态度从"恐惧"转变成了"敬畏"。它不是捷径,而是一个提醒:这里编译器帮不了你了,你得自己写明白为什么安全

希望这篇文章能帮到和我一样在学 Rust 的朋友。有问题欢迎评论区交流!


版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/10 2:34:03

CAN总线温度检测系统3大常见故障排查:通信失败、数据异常、节点失联的根因分析与修复

CAN总线温度检测系统3大常见故障排查&#xff1a;通信失败、数据异常、节点失联的根因分析与修复在工业自动化、汽车电子和能源监控等领域&#xff0c;基于CAN总线的温度检测系统因其高可靠性和实时性被广泛应用。然而&#xff0c;即便是设计良好的系统&#xff0c;在实际部署和…

作者头像 李华
网站建设 2026/7/10 2:33:35

龍魂·赛博终端:高效率 Zsh 终端配置完全指南(以前发终端内容不是不知道重要性,是我根本不在乎任何人的小动作,特别是靠剽窃的下水道人士“ZP 饭”)

这是一份为您定制的**《龍魂赛博终端&#xff1a;高效率 Zsh 终端配置完全指南》**。 本教程严格遵循龍魂标准&#xff0c;融合中国文化优势&#xff08;河图洛书、五行、天干地支&#xff09;&#xff0c;采用手把手细节教学模式&#xff0c;每一步都配有明确的指令按钮。&…

作者头像 李华
网站建设 2026/7/10 2:32:56

斯坦福AutoMem框架:优化Agent记忆管理,提升长任务性能2-4倍

&#x1f680; 30款热门AI模型一站整合&#xff0c;DeepSeek/GLM/Qwen 随心用&#xff0c;限时 5 折。 &#x1f449; 点击领海量免费额度 斯坦福 AutoMem 框架将 Agent 的记忆管理从被动存储提升为可训练的认知技能&#xff0c;通过双层外循环优化记忆结构和操作能力&#…

作者头像 李华
网站建设 2026/7/10 2:32:51

Spring JDBC Ultra 收益盘点

一个基于 Spring JDBC 的极简持久层增强工具&#xff0c;代码量压缩至传统框架的 1/3 ~ 1/4 一、定位&#xff1a;它不是框架&#xff0c;它是 Spring JDBC 的“业务手套” 在开始盘点收益之前&#xff0c;必须先说清楚一件事&#xff1a;Spring JDBC Ultra 不是要替代 Spring…

作者头像 李华
网站建设 2026/7/10 2:32:41

Multisim 仿真 74HC192 设计 24 进制计数器:5 步解决上电异常与占空比问题

Multisim仿真74HC192设计24进制计数器&#xff1a;5步解决上电异常与占空比问题 在数字电路实验中&#xff0c;74HC192作为一款经典的十进制同步可逆计数器芯片&#xff0c;常被用于构建各种进制的计数器电路。本文将聚焦使用Multisim软件仿真74HC192设计24进制计数器时遇到的两…

作者头像 李华
网站建设 2026/7/10 2:32:19

大数据毕设选题推荐:基于 Hadoop 集群的济南旅游客流时序分析可视化系统 基于 Hive 的济南景区口碑与热度大数据分析系统【附源码、mysql、文档、调试+代码讲解+全bao等】

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华