[!|center] 普若哥们儿
不安全 Rust
目前为止讨论过的代码都有 Rust 在编译时强制性的内存安全保证。然而,Rust 还有另一种语言规则,它不会强制执行这类内存安全保证:这被称为 不安全 Rust(unsafe Rust)。它与常规 Rust 代码无异,但是会提供额外的超能力。
不安全 Rust 之所以存在,是因为静态分析本质上是保守的。这必然意味着有时代码 可能 是合法的,但如果 Rust 编译器没有足够的信息来确定,它将拒绝该代码。在这种情况下,可以使用 unsafe 关键字告诉编译器,“相信我,我知道我在干什么”。不过要注意,使用不安全 Rust 风险自担,如果不安全代码出错了,比如解引用空指针,可能会导致不安全的内存使用。
另一个 Rust 存在不安全一面的原因是:底层计算机硬件固有的不安全性。如果 Rust 不允许进行不安全操作,那么有些任务则根本完成不了。Rust 需要能够直接与操作系统交互,甚至于编写操作系统这样的底层系统编程!这也是 Rust 语言的目标之一。
不安全的超能力
可以通过 unsafe 关键字来切换到不安全 Rust,接着可以开启一个存放不安全代码的块。这里有五类可以在不安全 Rust 中进行而不能用于安全 Rust 的操作,它们称之为 “不安全的超能力(unsafe superpowers)” 。这些超能力是:
- 解引用裸指针
- 调用不安全的函数或方法
- 访问或修改可变静态变量
- 实现不安全 trait
- 访问
union的字段
这就是全部了。注意,unsafe 并不意味着可以做任何不安全的操作,只有上述这 5 类。
有一点很重要,unsafe 并不会关闭借用检查器或禁用任何其他 Rust 安全检查:如果在不安全代码中使用引用,它仍会被检查。unsafe 关键字只是提供了那五个不会被编译器检查内存安全的功能,因此你仍然能在不安全块中获得某种程度的安全。
再者,unsafe 不意味着块中的代码就一定是危险的或者必然导致内存安全问题:其意图在于作为程序员你将会确保 unsafe 块中的代码以有效的方式访问内存。
保持 unsafe 块尽可能小,这样当错误发生时就容易在小范围内排查。
为了尽可能隔离不安全代码,将不安全代码封装进一个安全的抽象并提供安全 API 是一个好的设计。标准库中的一些功能通过 unsafe 代码实现,并封装为安全抽象。
下面顺序依次介绍上述五个超能力,同时我们会看到一些提供不安全代码的安全接口的抽象。
解引用裸指针
编译器会确保引用总是有效的。类似于引用,不安全 Rust 有两个被称为 裸指针(raw pointers)的新类型,可变裸指针和不可变裸指针,分别写作 *const T 和 *mut T。这里的星号不是解引用运算符;它是类型名称的一部分。在裸指针的上下文中,不可变 意味着指针解引用之后不能直接赋值。
裸指针与引用和智能指针的区别在于
- 允许忽略借用规则,可以同时拥有不可变和可变的指针,或多个指向相同位置的可变指针
- 不保证指向有效的内存
- 允许为空
- 不能实现任何自动清理功能
通过去掉 Rust 强加的保证,你可以换取性能或使用另一个语言或硬件接口的能力。
下例展示了如何从引用同时创建不可变和可变裸指针。
fn main() {
let mut num = 5;
let r1 = &num as *const i32; // 不可变裸指针
let r2 = &mut num as *mut i32; // 可变裸指针
}
注意这里没有引入 unsafe 关键字,因为在安全代码中 创建 裸指针是安全的,只是不能在不安全块之外 解引用 裸指针,稍后便会看到。
这里使用 as 将不可变和可变引用强转为对应的裸指针类型。因为直接从保证安全的引用来创建它们,因此这些特定的裸指针是有效,但是不能对任何裸指针做出如此假设。
作为展示,接下来会创建一个不能确定其有效性的裸指针,下例展示了如何创建一个指向任意内存地址的裸指针。尝试使用任意内存是未定义行为:此地址可能有也可能没有数据,编译器可能会优化掉这个内存访问,或者程序可能会出现段错误(segmentation fault)。通常没有好的理由编写这样的代码,不过却是可行的:
fn main() {
let address = 0x012345usize;
let r = address as *const i32;
}
记得我们说过可以在安全代码中创建裸指针,不过不能 解引用 裸指针和读取其指向的数据。现在我们要做的就是对裸指针使用解引用运算符 *,这需要一个 unsafe 块,如下例所示:
fn main() {
let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
unsafe {
//*r1 = *r1 + 1; //错误!
*r2 = *r1 + 1;
println!("r1 is: {}", *r1);
println!("r2 is: {}", *r2);
}
}
注意,创建一个指针不会造成任何危险;只有当访问其指向的值时才有可能遇到无效的值。
例中创建了同时指向相同内存位置 num 的裸指针 *const i32 和 *mut i32。相反如果尝试同时创建 num 的不可变和可变引用,将无法通过编译,因为 Rust 的所有权规则不允许在拥有任何不可变引用的同时再创建一个可变引用。通过裸指针,就能够同时创建同一地址的可变指针和不可变指针,若通过可变指针修改数据,则可能潜在造成数据竞争。请多加小心!
既然存在这么多的危险,为何还要使用裸指针呢?一个主要的应用场景便是调用 C 代码接口。另一个场景是构建借用检查器无法理解的安全抽象。让我们先介绍不安全函数,接着看一看使用不安全代码的安全抽象的例子。
调用不安全函数或方法
第二类可以在不安全块中进行的操作是调用不安全函数。不安全函数和方法与常规函数方法十分类似,除了其开头有一个额外的 unsafe。在此上下文中,关键字 unsafe 表示该函数具有调用时需要满足的要求,而 Rust 不会保证满足这些要求。通过在 unsafe 块中调用不安全函数,表明我们已经阅读过此函数的文档并对其是否满足函数自身的契约负责。
如下是一个没有做任何操作的不安全函数 dangerous 的例子:
fn main() {
unsafe fn dangerous() {}
unsafe {
dangerous();
}
}
必须在一个单独的 unsafe 块中调用 dangerous 函数。如果尝试不使用 unsafe 块调用 dangerous,则会得到一个错误。
通过 unsafe 块,我们向 Rust 保证了我们已经阅读过函数的文档,理解如何正确使用,并验证过其满足函数的契约。
不安全函数体也是有效的 unsafe 块,所以在不安全函数中进行另一个不安全操作时无需新增额外的 unsafe 块,比如 test 函数包含一个 unsafe 语句块:
fn main() {
let mut num = 5;
test(num);
}
fn test(mut num: i32) {
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
unsafe {
println!("r1 is: {}", *r1);
println!("r2 is: {}", *r2);
}
}
test 函数中包含 unsafe 语句块,而 test 函数没有标记 unsafe,因此 test 是普通的安全函数。
[!attention] 将不安全代码封装在一个安全函数中,在《rust programming language》中称之为“创建不安全代码的安全抽象”,讲的很啰嗦,其实就是一句话:将不安全代码封装在一个安全函数中。安全函数不是一个新概念,就是 Rust 中的没有标记为
unsafe的普通函数。
如果将 test 函数标记为 unsafe,则其中的不安全语句块不用再标记 unsafe,但是调用该函数的地方需要标记 unsafe,比如:
fn main() {
let mut num = 5;
unsafe {
test(num);
}
}
unsafe fn test(mut num: i32) {
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
println!("r1 is: {}", *r1);
println!("r2 is: {}", *r2);
}
有时你的 Rust 代码可能需要与其他语言编写的代码交互。为此 Rust 有一个extern关键字,用于创建和使用 外部函数接口(Foreign Function Interface,FFI)。外部函数接口是一个编程语言用以定义函数的方式,其允许不同(外部)编程语言调用这些函数。
下例展示了如何集成 C 标准库中的 abs 函数。extern 块中声明的函数在 Rust 代码中总是不安全的。因为其他语言不会强制执行 Rust 的规则并且 Rust 无法检查它们,所以确保其安全是程序员的责任:
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}
在 extern "C" 块中,列出了我们希望能够调用的另一个语言中的外部函数的签名和名称。"C" 部分定义了外部函数所使用的 应用二进制接口(application binary interface,ABI) —— ABI 定义了如何在汇编语言层面调用此函数。"C" ABI 是最常见的,并遵循 C 编程语言的 ABI。
也可以使用 extern 来创建一个允许其他语言调用 Rust 函数的接口。不同于创建整个 extern 块,除了在 fn 关键字之前增加 extern 关键字并为相关函数指定所用到的 ABI 之外,还需增加 #[no_mangle] 注解来告诉 Rust 编译器不要 mangle 此函数的名称。Mangling 发生于当编译器将我们指定的函数名修改为不同的名称之时,这会增加用于其他编译过程的额外信息,不过会使其名称更难以阅读。每一个编程语言的编译器都会以稍微不同的方式 mangle 函数名,所以为了使 Rust 函数能在其他语言中指定,必须禁用 Rust 编译器的 name mangling。
在如下的例子中,一旦将其编译为动态库,并从 C 语言中链接,call_from_c 函数就能够在 C 代码中调用:
#[no_mangle]
pub extern "C" fn call_from_c() {
println!("Just called a Rust function from C!");
}
extern 的使用无需 unsafe。
访问或修改可变静态变量
目前为止全书都尽量避免讨论 全局变量(global variables),Rust 支持全局变量,不过这对于 Rust 的所有权规则来说是有问题的,如果有两个线程访问相同的可变全局变量,则可能会造成数据竞争。
全局变量在 Rust 中被称为 静态(static)变量。下例展示了一个拥有字符串 slice 值的静态变量的声明和应用:
static HELLO_WORLD: &str = "Hello, world!";
fn main() {
println!("name is: {}", HELLO_WORLD);
}
静态(static)变量类似于常量。静态变量只能储存拥有 'static 生命周期的引用,这意味着 Rust 编译器可以自己计算出其生命周期而无需显式标注。访问不可变静态变量是安全的。
常量与不可变静态变量的区别:
- 静态变量中的值有一个固定的内存地址,使用这个值总是会访问相同的地址,而常量只是一个字面量值,在任何被用到的时候将值内嵌入代码。
- 另一个区别在于静态变量可以是可变的,访问和修改可变静态变量都是 不安全 的。
下例展示了如何声明、访问和修改名为 COUNTER 的可变静态变量:
static mut COUNTER: u32 = 0;
fn add_to_count(inc: u32) {
unsafe {
COUNTER += inc;
}
}
fn main() {
add_to_count(3);
unsafe {
println!("COUNTER: {}", COUNTER);
}
}
就像常规变量一样,我们使用 mut 关键来指定可变性。任何读写 COUNTER 的代码都必须位于 unsafe 块中。这段代码可以编译并如期打印出 COUNTER: 3,因为这是单线程的。拥有多个线程访问 COUNTER 则可能导致数据竞争。
拥有可以全局访问的可变数据,难以保证不存在数据竞争,这就是为何 Rust 认为可变静态变量是不安全的。任何可能的情况,请优先使用并发技术和线程安全智能指针,这样编译器就能检测不同线程间的数据访问是否是安全的。
实现不安全 trait
unsafe 的另一个操作用例是实现不安全 trait。当 trait 中至少有一个方法中包含编译器无法验证的 trait 时是不安全的。可以在 trait 之前增加 unsafe 关键字将 trait 声明为 unsafe,同时 trait 的实现也必须标记为 unsafe,如下例所示:
unsafe trait Foo {
// methods go here
}
unsafe impl Foo for i32 {
// method implementations go here
}
fn main() {}
通过 unsafe impl,我们承诺将保证编译器所不能验证的 trait。
编译器会自动为完全由 Send 和 Sync 类型组成的类型自动实现它们。如果实现了一个包含一些不是 Send 或 Sync 的类型,比如裸指针,并希望将此类型标记为 Send 或 Sync,则必须使用 unsafe。Rust 不能验证我们的类型保证可以安全的跨线程发送或在多线程间访问,所以需要我们自己进行检查并通过 unsafe 表明。
为裸指针实现Send
前面我们提到裸指针既没实现 Send 也没有 Sync,意味着下面代码会报错:
use std::thread;
fn main() {
let p = 5 as *mut u8;
let t = thread::spawn(move || {
println!("{:?}",p);
});
t.join().unwrap();
}
报错跟之前无二: `*mut u8` cannot be sent between threads safely, 但是有一个问题,我们无法为其直接实现Send特征,好在可以用newtype类型 :struct MyBox(*mut u8);。
复合类型中有一个成员没实现Send,该复合类型就不是Send,因此我们需要手动为它实现:
use std::thread;
#[derive(Debug)]
struct MyBox(*mut u8);
unsafe impl Send for MyBox {}
fn main() {
let p = MyBox(5 as *mut u8);
let t = thread::spawn(move || {
println!("{:?}",p);
});
t.join().unwrap();
}
有一点需要注意:Send和Sync是unsafe特征,实现时需要用unsafe代码块包裹。
为裸指针实现Sync
由于Sync是多线程间共享一个值,大家可能会想这么实现:
use std::thread;
fn main() {
let v = 5;
let t = thread::spawn(|| {
println!("{:?}",&v);
});
t.join().unwrap();
}
关于这种用法,在多线程章节也提到过,线程如果直接去借用其它线程的变量,会报错:closure may outlive the current function,, 原因在于编译器无法确定主线程main和子线程t谁的生命周期更长,特别是当两个线程都是子线程时,没有任何人知道哪个子线程会先结束,包括编译器!
因此我们得配合Arc去使用:
use std::thread;
use std::sync::Arc;
use std::sync::Mutex;
#[derive(Debug)]
struct MyBox(*const u8);
unsafe impl Send for MyBox {}
fn main() {
let b = &MyBox(5 as *const u8);
let v = Arc::new(Mutex::new(b));
let t = thread::spawn(move || {
let _v1 = v.lock().unwrap();
});
t.join().unwrap();
}
上面代码将智能指针v的所有权转移给新线程,同时v包含了一个引用类型b,当在新的线程中试图获取内部的引用时,会报错:
error[E0277]: `*const u8` cannot be shared between threads safely
--> src/main.rs:25:13
|
25 | let t = thread::spawn(move || {
| ^^^^^^^^^^^^^ `*const u8` cannot be shared between threads safely
|
= help: within `MyBox`, the trait `Sync` is not implemented for `*const u8`
因为我们访问的引用实际上还是对主线程中的数据的借用,转移进来的仅仅是外层的智能指针引用。要解决很简单,增加一行代码,为 MyBox 实现 Sync:
unsafe impl Sync for MyBox {}
总结
通过上面的两个裸指针的例子,我们了解了如何实现Send和Sync,以及如何只实现Send而不实现Sync,简单总结下:
- 实现
Send的类型可以在线程间安全的传递其所有权, 实现Sync的类型可以在线程间安全的共享(通过引用) - 绝大部分类型都实现了
Send和Sync,常见的未实现的有:裸指针、Cell、RefCell、Rc等 - 可以为自定义类型实现
Send和Sync,但是需要unsafe代码块 - 可以为部分 Rust 中的类型实现
Send、Sync,但是需要使用newtype,例如文中的裸指针例子
访问联合体中的字段
union 和 struct 类似,但是在一个实例中同时只能使用一个声明的字段。联合体主要用于和 C 代码中的联合体交互。访问联合体的字段是不安全的,因为 Rust 无法保证当前存储在联合体实例中数据的类型。
#[repr(C)]
union MyUnion {
f1: u32,
f2: f32,
}
fn main() {
let u = MyUnion { f1: 10 };
let f = unsafe { u.f2 };
let mut g = 0u32;
unsafe {
g = u.f1;
}
println!("{}", f);
println!("{}", g);
unsafe {
match u {
MyUnion { f1: 10 } => { println!("ten"); }
MyUnion { f2 } => { println!("{}", f2); }
}
}
}