Unsafe Rust 能做什么

Unsafe Rust 唯一不同的地方在于你可以

  • 解引用裸指针
  • 调用 unsafe 函数(包括 C 函数、编译器 intrinsic 函数和裸分配器)
  • 实现 unsafe trait
  • 访问或修改可变 static 变量
  • 访问 union 的字段

就是这些。这些操作被限制在 Unsafe 中的原因是,误用其中任何一个都将导致备受争议的未定义行为(Undefined Behavior)。触发未定义行为会赋予编译器对你的程序执行任意糟糕操作的完全权利。你绝对不应该触发未定义行为。

与 C 不同,未定义行为在 Rust 中的范围相当有限。核心语言关心的只是阻止以下事情发生:

  • 解引用(使用 * 运算符对)悬垂或未对齐的指针(见下文)
  • 破坏 指针别名规则
  • 使用错误的调用 ABI 调用函数,或从使用错误 unwind ABI 的函数中 unwinding。
  • 导致 数据竞争
  • 执行使用当前执行线程不支持的 目标特性(target features)编译的代码
  • 产生无效值(无论是单独的,还是作为复合类型如 enum/struct/array/tuple 的字段)
    • 非 0 或 1 的 bool
    • 带有无效 discriminant 的 enum
    • fn 指针
    • 不在 [0x0, 0xD7FF] 和 [0xE000, 0x10FFFF] 范围内的 char
    • 一个 ! (此类型的所有值都无效)
    • 未初始化内存 中读取的整数 (i*/u*)、浮点值 (f*) 或裸指针,或 str 中的未初始化内存。
    • 悬垂、未对齐或指向无效值的引用/Box
    • 带有无效元数据的宽引用、Box 或裸指针
      • dyn Trait 元数据无效,如果它不是指向与指针或引用指向的实际动态 trait 匹配的 Trait vtable 的指针
      • slice 元数据无效,如果长度不是有效的 usize(即,它不能从未初始化内存中读取)
    • 具有自定义无效值且是其中一个值的类型,例如为空的 NonNull。(请求自定义无效值是一个不稳定的特性,但一些稳定的 libstd 类型,如 NonNull,使用了它。)

关于“未定义行为”的更详细解释,请参阅 参考手册

“产生”一个值发生在任何时候一个值被赋值、传递给函数/基本操作,或从函数/基本操作返回。

如果引用/指针为空或它指向的所有字节不属于同一分配块(因此它们都必须属于某个分配块),则称其为“悬垂”。它指向的字节跨度由指针值和被指向类型的大小决定。因此,如果跨度为空,“悬垂”与“空”相同。请注意,slice 和 string 指向其整个范围,因此长度元数据绝不能过大(特别是,分配以及因此的 slice 和 string 不能大于 isize::MAX 字节)。如果出于某种原因这太麻烦,请考虑使用裸指针。

就是这些。这些是 Rust 中固有的所有未定义行为的原因。当然,unsafe 函数和 trait 可以自由地声明程序必须遵守的任意其他约束来避免未定义行为。例如,分配器 API 声明释放未分配的内存是未定义行为。

然而,违反这些约束通常只会间接导致上述问题之一。一些额外的约束也可能源于编译器 intrinsic 函数,这些函数对代码如何优化做出了特殊假设。例如,Vec 和 Box 使用了一些 intrinsic 函数,要求它们的指针始终是非空的。

否则,Rust 对于其他可疑操作相当宽松。Rust 认为以下操作是“安全的”:

  • 死锁
  • 存在 竞态条件
  • 内存泄漏
  • 整数溢出(使用内置运算符如 + 等)
  • 中止程序
  • 删除生产数据库

有关更详细的信息,请参阅 参考手册

然而,任何实际设法做这种事情的程序可能是错误的。Rust 提供了许多工具来使这些事情变得罕见,但这些问题被认为在范畴上难以完全阻止。