安全与不安全如何交互

那么,那么安全Rust与不安全Rust是什么关系?它们又如何交互?

Rust用unsafe关键字来模式化安全与不安全Rust的分别,它可以被理解为一种安全与不安全Rust之间的外部函数接口(FFI)。这是为何我们能够说安全Rust是一个安全语言背后的魔术:就像其它每一个安全语言那样,所有可怕的不安全部分都专门归类到了FFI中。

然而因为一个语言(安全Rust)是另一个语言(不安全Rust)的一个子集,只要安全与不安全Rust之间的边界使用unsafe关键字表示,这两者就可以有规则的混合。不需要写头文件,初始化运行时,或者任何其它FFI模板。

这是目前Rust中unsafe可以出现的地方,它们大体上可以分为两类:

  • 当这里有未检查的协议时。为了表明你理解了它,我们要求你在这些地方使用unsafe
    • 对于函数,unsafe表明函数调用时不安全的。函数的用户必须检查文档来确定它的意义,并必须使用unsafe来表明他们意识到了危险。
    • 对于trait声明,unsafe表明实现这个trait是一个不安全操作,因为它包含其它不安全代码可以盲目信任的协议。(接下来介绍更多。)
  • 当我表明,据我所知,我坚持了未检查的协议:
    • 对于trait实现,unsafe表明unsafetrait包含的协议。
    • 对于代码块,unsafe表明一个不安全操作多包含的任何不安全性将被处理,并且因此父函数是安全的。

也有一个#[unsafe_no_drop_flag],它是一个因为历史原因而存在的特例,并且正在被逐步淘汰的过程中。查看丢弃的标记(drop flags)章节来获得更多细节。

一些不安全函数的例子:

  • slice::get_unchecked进行未检查的索引,允许内存安全被随意违反。
  • ptr::offset是一个固有功能(intrinsic),它可以在没有被LLVM定义为“界内”时调用未定义行为(注:可以越界索引)。
  • mem::transmute用给定的类型重新解释一些值,用任意的方式绕过类型安全。(更多细节位于转换
  • 所有FFI函数都是unsafe因为它们可以做任何事。C语言就是一个明显的当事人,不过通常任何语言都可能做让Rust不高兴的事。(黑其它语言是高阶Rust的一大特点吗。。。)

截至Rust 1.0这里就有两个不安全trait:

  • Send是一个标记trait(它没有实际的API),它保证其实现可以安全的发送(移动)到另一个线程。
  • Sync是一个标记trait,它保证线程可以通过一个共享引用来安全的共享其实现。

对不安全trait的需求归结于安全代码的基本属性:

哪怕是糟糕到彻底的安全代码,它也不能造成未定义行为。

这意味着不安全代码,未定义行为的忠诚先锋,必须对通常的安全代码持完全的怀疑态度。不安全代码也可以随意相信特定的安全代码(否则你将会陷入偏执狂绝望的无限循环中)。通常相信标准库是正确被认为是OK的,因为std基本上是一个语言的扩展(并且你真的仅仅需要相信语言本身)。如果std未能保证它所声明的东西,那么这基本上是一个语言bug。

这就是说,最好最小化对具体安全代码属性不必要的依赖。bug总会出现!当然,我们必须强调这仅仅是对不安全代码的一个担心。只要基本的内存安全被考虑到了,安全代码可以无条件的信任所有任何人。

另一方面,安全trait能随意声明任意的协议,不过因为可以安全的实现它们,不安全代码不能相信这些协议是否确实被实现了。这因具体情况而不同,因为任何人可以任意的实现这些接口。这就是所谓相信一段具体代码是正确的,和相信所有的代码将被会被正确编写的本质区别。

例如Rust有PartialOrdOrdtraitl来区别那些“仅仅”能比较的类型和实际实现了一个完整顺序的类型。差不多所有处理可比较数据的API真正需要Ord的数据。比如,一个像BTreeMap这样的排序map甚至对部分排序的数据是没有意义的。如果你声明你对一个类型实现了Ord,不过实际上并没有提供一个合适的完整的排序功能,BTreeMap将会陷入混乱并搞得一团糟。插入的数据可能不能被找到!

不过这是可以的。BTreeMap是安全的,所以它保证了即便你给出了一个简直是垃圾的Ord实现,它也会保持安全。你不会开始读到未初始化内存或者未分配的内存。事实上,BTreeMap能够并不丢失你的任何数据。当map被丢弃时,所有的析构函数将被成功调用!万岁!

然而BTreeMap用了适量的不安全代码来实现(大部分集合类型都是)。这意味着一个坏的Ord实现将使BTreeMap安全的执行不见得是对的,不安全代码必须确保在安全得不到保证时不依赖OrdOrd是由安全代码提供的,并且保证安全性并不是安全代码的责任。

不过如果有什么方法在一些地方让不安全代码可以信任一些trait协议岂不是很好?这是不安全trait要解决的问题:通过标记trait本身的实现是不安全的,不安全代码可以信任它的实现保证了trait的协议。即便trait的实现在任何其他什么情况下可能是不正确的。

例如,给定一个假设的UnsafeOrdtrait,这从技术上讲是一个有效的实现:

  1. unsafe impl UnsafeOrd for MyType {
  2. fn cmp(&self, other: &Self) -> Ordering {
  3. Ordering::Equal
  4. }
  5. }

不过这可能不是你想要的实现。

Rust有避免产生不安全trait的传统,因为它会让不安全代码到处都是,这并不是很理想的。SendSync是不安全的,因为线程安全是一个基本属性,不过可能并不能期望不安全代码像抵御一个坏的Ord实现那样使我们免受线程安全的困扰。避免线程不安全的唯一可能方法就是完全不要使用线程。让所有的读取和存储都是原子的并不足够,因为它可能导致内存中不相交的位置出现复杂的不可变量。例如,指针和vector的实际存储必须是Sync的。

即便是像消息传递这样传统上被认为是完全安全的并发范式也隐式的依赖一些线程安全的概念 — 传递一个指针时你真的是在传递消息吗?SendSync因此要求一些安全代码不能提供的基本层次的信任,所以他们必须被不安全的实现。为了帮助消除他们将引入的普遍的不安全性,Send(相应的Sync)会自动导出所有只由Send(相应的Sync)值组成的类型。99%的类型是SendSync的,并且这99%从来不表现出来(剩下的1%是无法避免的同步原语(synchronization primitives))。