参考章节《Rust程序设计语言》第19.1章 不安全 Rust
参考章节《Rust语言圣经(Rust Course)》第3.9.1章 五种兵器
目前为止讨论过的代码都有 Rust 在
编译时
会强制执行的内存安全检查
。然而,Rust 还隐藏有第二种语言,它不会强制执行这类内存安全检查
说白了,我们可以让Rust不执行内存安全检查,这被称为 不安全 Rust(unsafe Rust)
。
此时,你就必须对代码的正确性负责,如果不安全代码出错了,比如解引用空指针,可能会导致不安全的内存使用。
我们来看看它都有哪些魔法吧
- 解引用裸指针
- 调用不安全的函数或方法
- 访问或修改可变静态变量
- 实现不安全 trait
- 访问 union 的字段
|
|
可以通过
unsafe
关键字来切换到不安全 Rust
,接着可以开启一个新的存放不安全代码的块。
- 解引用裸指针
不安全 Rust 有两个被称为
裸指针(raw pointers)
的类似于引用的新类型。
和引用一样,裸指针是不可变或可变的,分别写作*const T
和*mut T
。这里的*
不是解引用运算符;它是类型名称的一部分。
在裸指针的上下文中,不可变意味着指针解引用之后不能直接赋值。
下面的代码基于引用创建了一个不可变裸指针和可变裸指针
|
|
细心的同学可能会发现,在创建裸指针时,并没有
unsafe
的身影,原因在于:创建裸指针是安全的行为,而解引用裸指针
才是不安全的行为
至此,我们已经学过三类指针,分别是引用,智能指针,和裸指针,而裸指针和它们的区别在于
- 裸指针允许忽略借用规则,可以同时拥有一个数据的可变、不可变指针,甚至还能拥有多个可变的指针
- 不保证指向有效的内存
- 允许为空(null)
- 没有实现任何自动的回收 (drop)
你还可以基于任意内存地址创建裸指针
不过这么做是
非常危险的
,因为该内存地址有可能存在值,也有可能没有,就算有值,也大概率不是你需要的值,总之,不要这么做,虽然它是可行的。
|
|
- 调用不安全的函数或方法
unsafe
函数从外表上来看跟普通函数并无区别,唯一的区别就是它需要使用 unsafe fn
来进行定义。
这种定义方式是为了告诉调用者,当调用此函数时,你需要注意它的相关需求,因为 Rust 无法担保调用者在使用该函数时能满足它所需的一切需求。
|
|
尝试运行上面这段代码
|
|
要调用不安全的函数,必须使用 unsafe
块包起来
这么做是为了让你清楚的认识到,正在调用的是一个不安全的函数,需要小心看看文档,看看函数有哪些特别的要求需要被满足。
|
|
- 创建不安全代码的安全抽象
我们来看这样一个函数,它传入一个数组,和其中间元素的索引,然后将它分成两个切片,并且每个切片都是可变的
|
|
上面的代码看起来很合理,但由于借用规则,不允许借用同一个Slice两次,因此运行会报错
|
|
Rust 的借用检查器不能理解我们要借用这个 slice
的两个不同部分,它只知道我们借用了同一个 slice
两次。
而本质上借用 slice
的不同部分是可以的,因为结果两个 slice
不会重叠,不过 Rust 还没有智能到能够理解这些。
当我们知道某些事是可以的而 Rust 不知道的时候,就是触及不安全代码的时候了
|
|
虽然 split_at_mut
使用了 unsafe
,但我们无需将整个函数声明为 unsafe fn
,因为我们在函数内部保证了指针的安全。
至此,我们就创建了一个不安全代码的安全抽象,其代码以一种安全的方式使用了 unsafe
代码
- 使用 extern 函数调用外部代码
有时你的 Rust 代码可能需要与其他语言编写的代码交互。
为此 Rust 有一个关键字,extern
,有助于创建和使用外部函数接口(Foreign Function Interface, FFI)
。
但并不是所有语言都这么叫,例如Java叫做 JNI(Java Native Interface)
外部函数接口是一个编程语言用以定义函数的方式,其允许不同(外部)编程语言调用这些函数。
下面代码演示了如何调用 C
标准库中的 abs
函数
|
|
在
extern "C"
块中,列出了我们希望能够调用的另一个语言中的外部函数的签名和名称。
"C"
部分定义了外部函数所使用的应用二进制接口(application binary interface,ABI)
ABI
定义了如何在汇编语言层面调用此函数。"C"
ABI 是最常见的,表示遵循 C 编程语言的 ABI。
当然,除了调用其他语言提供的FFI函数
以外,你还可以创建一个FFI函数
以供其他语言调用
|
|
上面的代码可以让 call_from_c
函数被 C
语言的代码调用,当然,前提是将其编译成一个共享库,然后链接到 C 语言中。
这里还有一个比较奇怪的注解
#[no_mangle]
,它用于告诉 Rust 编译器:不要乱改函数的名称。
默认情况下,Rust 在编译时会去修改函数的名称,为了让名称包含更多的信息,这样其它的编译部分就能从该名称获取相应的信息
这种修改会导致函数名变得相当不可读。因此,为了让 Rust 函数能顺利被其它语言调用,我们必须要禁止掉该功能。
- 访问或修改可变静态变量
Rust 认为访问或修改可变静态变量是不安全的。因为拥有可以全局访问的可变数据,难以保证不存在数据竞争
你可能会说,修改是不安全的,怎么访问也算呢?原因很简单,可变静态变量意味着只要你能访问到这个数据,你就有权限改,Rust 可不管你到底会不会改
|
|
就像常规变量一样,我们使用 mut
关键来指定可变性。任何读写 COUNTER
的代码都必须位于 unsafe
块中。
这段代码可以编译并如期打印出 COUNTER: 3
,因为这是单线程的。拥有多个线程访问 COUNTER
则可能导致数据竞争。
- 实现不安全 trait
当
trait
中至少有一个方法中包含编译器无法验证的内容时trait
是不安全的。
关于不安全的 trait
可以参考 《Rust语言圣经(Rust Course)》为裸指针实现 Sync
这一节
要实现不安全的 trait
也很简单,使用 unsafe impl
关键字即可,我们告诉编译器,相应的正确性由我们自己来保证。
|
|
- 访问联合体中的字段
仅适用于 unsafe 的最后一个操作是访问 联合体 中的字段,union 和 struct 类似,但是在一个实例中同时只能使用一个声明的字段。
联合体主要用于和 C 代码中的联合体交互。访问联合体的字段是不安全的,因为 Rust 无法保证当前存储在联合体实例中数据的类型。
参考 https://doc.rust-lang.org/reference/items/unions.html#unions
总结一下
unsafe
只应该用于这五种场景,其它场景,你应该坚决的使用安全的代码。unsafe
虽然为我们提供了一些额外的能力,但需要我们自己对内存安全负责,因此我们必须要异常小心。