unsafe 是 Rust 里最容易被误解的关键字。它不是"关闭安全检查"的开关,而是把特定操作的内存安全责任从编译器移交给你。本文用 6 个常见误用示例说明它的边界和正确用法。

unsafe 到底是什么

unsafe 块允许你执行 5 类操作,除此之外,借用检查、所有权规则等所有其他规则在 unsafe 块内依然生效

  1. 解引用裸指针
  2. 调用 unsafe 函数或外部函数(FFI)
  3. 访问或修改可变的静态变量
  4. 访问 union 的字段
  5. 实现 unsafe trait

编译器不再检查这 5 类操作的正确性,这意味着你需要自己保证它们安全,并在代码中写清楚依据(SAFETY 注释)。

误用 1:用 unsafe 封住编译器的嘴

最常见的错误:搞不定生命周期或所有权问题时,用 unsafe 块包起来让编译器"闭嘴"。

let r: &i32;
unsafe {
    r = std::mem::transmute(0x123456usize);
}
println!("{r}");

transmute 把任意地址强转成 &i32 引用。编译通过,运行大概率崩溃;更糟的情况是它"碰巧"没崩,但数据已经损坏,问题会在完全无关的地方暴露。

正确做法:先理解生命周期和所有权的报错。编译器报错不是障碍,是信息。unsafe 不能解决所有权问题,只能掩盖它。

误用 2:解引用任意裸指针

let ptr = 0x123456usize as *const i32;

unsafe {
    println!("{}", *ptr);
}

固定地址上可能什么都没有,可能是内核数据,也可能是其他进程的内存。解引用不是"寻宝",是未定义行为。

正确做法:只解引用你亲手创建、生命周期已知的指针,并写 SAFETY 注释说明依据:

let x = 42;
let ptr = &x as *const i32;

unsafe {
    // SAFETY: ptr 指向 x,x 在此处仍存活。
    println!("safe ptr: {}", *ptr); // 输出: 42
}

误用 3:以为 unsafe 内可以违反借用规则

unsafe 只豁免那 5 类操作,借用检查依然生效。下面的代码在 unsafe 块内也无法编译

unsafe {
    let mut v = vec![1, 2, 3];

    let x = &v[0];   // 不可变借用
    v.push(4);       // 可变借用,冲突
    println!("{x}");
}

同时持有 &v[0]v.push(4) 违反借用规则,unsafe 不会让你绕过它。

误用 4:重复造轮子

需要和 C 字符串交互时,手写 C 风格 strlen 是典型的"重新发明方形轮子":

unsafe fn strlen(ptr: *const u8) -> usize {
    let mut len = 0;
    while *ptr.add(len) != 0 {
        len += 1;
    }
    len
}

正确做法:标准库已经封装好,把 unsafe 限制在最小范围:

use std::ffi::CStr;

fn main() {
    let c_string_bytes = b"hello\0";
    let c_str_ptr = c_string_bytes.as_ptr() as *const i8;

    let cstr = unsafe {
        // SAFETY: 指针来源可靠,内容是以 \0 结尾的有效 C 字符串。
        CStr::from_ptr(c_str_ptr)
    };

    println!("CStr: {:?}", cstr);
}

写任何 unsafe 代码前先问自己:标准库或成熟 crate 是否已有安全封装?

误用 5:滥用 unsafe impl Send / Sync

unsafe impl Send for MyType {} 是对编译器的承诺:“这个类型可以安全地在线程间传递,不会出现数据竞争。“编译器完全信任你,不做任何检查。

如果 MyType 内部包含 Rc、裸指针或非线程安全的内部可变性,这个承诺就是错的——你会在运行时得到数据竞争,且极难排查。

正确做法:只有当你确认类型的所有内部状态在跨线程访问下都安全时,才实现 unsafe impl Send/Sync,并在注释中说明每个字段为什么安全。

误用 6:缺少 SAFETY 注释

每个 unsafe 块都应该有一段 // SAFETY: 注释,说明为什么这里的操作是安全的:指针来源、生命周期、不变量、外部约束。这不是形式主义——它是给未来的维护者(包括三个月后的你)看的证据链。

社区规范可参考 clippy::undocumented_unsafe_blocks:unsafe 块没有 SAFETY 注释直接告警。

什么时候真的需要 unsafe

  • FFI:调用 C/C++ 库,这是最普遍的正当场景。
  • 性能关键路径:有基准数据证明安全抽象是瓶颈,且 unsafe 版本通过审查。
  • 底层数据结构:自引用结构、无锁队列等安全抽象表达不了的场景。

判断标准很简单:先写安全代码,用基准证明它是瓶颈,再考虑 unsafe,且每个 unsafe 块都要有充分的 SAFETY 注释。

FAQ

unsafe 会关闭 Rust 的所有安全检查吗?

不会。unsafe 只额外允许 5 类操作:解引用裸指针、调用 unsafe/FFI 函数、访问或修改可变静态变量、访问 union 字段、实现 unsafe trait。借用检查和所有权规则在 unsafe 块内依然生效。

unsafe 块内可以违反借用规则吗?

不可以。在 unsafe 内同时持有同一数据的可变与不可变借用依然编译失败,unsafe 只豁免那 5 类操作,其余规则不变。

什么时候才应该用 unsafe?

三种正当场景:FFI 调用 C/C++ 库、有基准数据证明安全抽象是瓶颈的性能关键路径、自引用结构或无锁队列等安全抽象表达不了的底层数据结构。先写安全代码,用基准证明瓶颈后再考虑 unsafe。

SAFETY 注释是必须的吗?

社区规范要求每个 unsafe 块写 // SAFETY: 注释,说明指针来源、生命周期和不变量;clippy 的 undocumented_unsafe_blocks 检查会对缺少注释的 unsafe 块直接告警。