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) - ResultString, 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 NodeT { value: T, prev: OptionNonNullNodeT, // 用 NonNull 而不是裸指针语义更清晰 next: OptionNonNullNodeT, } implT NodeT { fn new(value: T) - Self { Node { value, prev: None, next: None, } } /// 在当前节点后插入新节点 fn insert_after(mut self, mut new_node: BoxNodeT) { // 安全前提调用者保证 self 在链表中的位置是确定的 unsafe { // 1️⃣ 设置新节点的前后指针 new_node.prev Some(NonNull::new_unchecked(self as *mut NodeT)); 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: Vecu8, } 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 包安全的四条铁律铁律 1unsafe 块越小越好// ❌ 不好 —— 整个函数都是 unsafe分不清哪里不安全 unsafe fn process_data(data: *const u8, len: usize) - Vecu8 { 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) - Vecu8 { // 参数校验在 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]) - ResultString, String { // 在可能导致 unsound 之前用安全的检查兜底 String::from_utf8(bytes.to_vec()) .map_err(|e| format!(无效的 UTF-8 编码: {e})) }铁律 3用类型系统把 unsafe关在笼子里use std::marker::PhantomData; /// 一个被验证过的原始指针包装类型 /// /// 这个类型的存在本身就意味着持有它的人已经确认过指针的有效性。 /// 外界无法直接构造必须通过 validate 方法。 pub struct ValidatedPtrT { ptr: *const T, _phantom: PhantomDataT, } implT ValidatedPtrT { /// 唯一构造入口 —— 验证指针非空 pub fn validate(ptr: *const T) - OptionSelf { 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 用法。当你自己实现Send或Synctrait 时相当于你在对编译器说这个类型跨线程传输是安全的我保证。use std::cell::UnsafeCell; use std::sync::Arc; /// 一个线程安全的计数器实现类似标准库的 AtomicUsize这里是教学简化版 struct MyAtomicUsize { // UnsafeCell 是 !Sync 的所以包含它的结构体默认不是 Sync inner: UnsafeCellusize, } // ⚠️ 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 的核心梳理了一遍unsafe 只放开五件事裸指针解引用、调用 unsafe 函数、访问可变静态变量、实现 unsafe trait、访问 union 字段。其余 Rust 安全检查照常工作。四个绕不开的 unsafe 场景FFI 调用 C 库、高性能自定义数据结构、硬件/内存映射 I/O、极少见的借用规则突破。除此之外绝大多数场景都不需要 unsafe。四条包装铁律unsafe 块尽量小、安全前提必须可证明、用类型系统把 unsafe 关在笼子里、不确定就别用。unsafe traitSend/Sync是你对编译器的郑重承诺写之前要想清楚我拿了什么同步机制来保证。作为独立开发者我对 unsafe 的态度从恐惧转变成了敬畏。它不是捷径而是一个提醒这里编译器帮不了你了你得自己写明白为什么安全。希望这篇文章能帮到和我一样在学 Rust 的朋友。有问题欢迎评论区交流