Rust的内存安全保证(没有运行时开销)是其独特的卖点;它是任何其他主流语言中没有的Rust语言功能。这些保证是有代价的:编写Rust需要您重新组织代码以调整借入检查器,并精确指定您使用的参考类型。
Unsafe Rust是Rust语言的超集,它削弱了其中一些限制以及相应的保证。在带有unsafe
关键字的块前缀会将该块切换到不安全模式,这允许正常Rust不支持的东西。特别是,它允许使用更像旧式C指针的原始指针。这些指针不受借阅规则的约束,程序员有责任确保它们在被取消引用时仍然指向有效内存。
因此,从表面上看,这个项目的建议是微不足道的:如果你只是在Rust中编写C代码,为什么要转到Rust?然而,在某些情况下,绝对需要unsafe
代码:对于低级库代码,或者当您的Rust代码必须与其他语言的代码接口时。
不过,此项目的措辞相当精确:避免编写unsafe
代码。重点是“写作”,因为很多时候,您可能需要的unsafe
代码已经为您编写了。
Rust标准库包含许多unsafe
代码;快速搜索在alloc
库中发现了大约1000个unsafe
的用途,在core
的1500个,在std
中还有2000个。该代码由专家编写,并因在数千个Rust代码库中使用而变得强硬。
其中一些unsafe
代码发生在我们已经介绍过的标准库功能的封面下:
- 第8项中描述的智能指针类型——
Rc
、RefCell
、Arc
和friends——在内部使用unsafe
代码(通常是原始指针),以便能够向用户呈现其特定语义。 - 下一篇中的同步原语——
Mutex
、RwLock
和相关保护——在内部使用unsafe
的操作系统特定代码。如果您想了解这些原语所涉及的微妙细节,建议使用Mara Bos(O'Reilly)的Rust Atomics和Locks。
标准库还具有其他功能,涵盖更高级的功能,在内部unsafe
的情况下实现:1
- std::pin::Pin强制项目不在内存中移动(项目15)。这允许自我参考的数据结构,通常是新来者对Rust的bête noire。
- std::borrow::Cow提供写时克隆智能指针:同一指针可用于读取和写入,并且只有当发生写入时,才会发生基础数据的克隆。
- std::mem中的各种功能(take、swap、replace)允许操作内存中的项目,而不会与借入检查器相冲突。
这些功能可能仍然需要谨慎才能正确使用,但unsafe
的代码已被封装,以消除整类问题的方式。
超越标准库,crates.io生态系统还包括许多封装unsafe
代码的板条箱,以提供常用功能:
- once_cell:提供一种具有全局变量的方法,正好初始化一次。
- rand:提供随机数生成,利用操作系统和CPU提供的低级底层功能。
- byteorder:允许将原始数据字节转换为数字和从数字转换。
- cxx:允许C++代码和Rust代码互操作。
还有很多其他的例子,但希望总体想法是明确的。如果您想做一些明显不符合Rust约束的事情,请浏览标准库,看看是否有现有功能可以满足您的需要。如果你没有找到你需要的东西,也可以尝试通过crates.io
进行狩猎。毕竟,遇到一个其他人从未遇到过的独特问题是不寻常的。
当然,总会有unsafe
的地方是被迫的,例如,当您需要通过外函数接口(FFI)与其他语言编写的代码进行交互时。但当有必要时,考虑编写一个包装层,其中包含所需的所有unsafe
代码,以便其他程序员可以遵循本项目中给出的建议。这也有助于定位问题:当出现问题时,unsafe
的包装可能是第一个嫌疑人。
此外,如果您被迫编写unsafe
代码,请注意关键字本身隐含的警告:Hic sunt dracones。
- 添加安全注释,记录
unsafe
代码所依赖的先决条件和不变量。Clippy有一个警告来提醒你这一点。 -
尽量减少不安全块中包含的代码量,以限制错误的潜在爆炸半径。考虑启用unsafe_op_in_unsafe_fn检测,以便在执行不安全操作时需要显式的unsafeblock,即使这些操作是在本身不安全的函数中执行的。
- 编写比平时更多的测试。
- 在代码上运行其他诊断工具。特别是,考虑在您的
unsafe
代码上运行Miri——Miri解释编译器的中级输出,这允许它检测Rust编译器不可见的错误类。 - 仔细考虑多线程的使用,特别是如果有共享状态。
添加unsafe
标记并不意味着不适用规则——这意味着您(程序员)现在负责维护Rust的安全保证,而不是编译器。
1在实践中,大多数std
功能实际上是由core
提供的,因此可用于之后文章中描述的no_std
代码。