Unsafe

Unsafe

unsafe trait

最常见的unsafe trait是Send/Sync

1
2
pub unsafe auto trait Send {}
pub unsafe auto trait Sync {}

任何trait,只要声明称unsafe,它就是一个unsafe trait.一个正常的trait也可以包含unsafe 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 实现这个 trait 的开发者要保证实现是内存安全的
unsafe trait Foo {
fn foo(&self);
}

trait Bar {
// 调用这个函数的人要保证调用是安全的
unsafe fn bar(&self);
}

struct Nonsense;

unsafe impl Foo for Nonsense {
fn foo(&self) {
println!("foo!");
}
}

impl Bar for Nonsense {
unsafe fn bar(&self) {
println!("bar!");
}
}

fn main() {
let nonsense = Nonsense;
// 调用者无需关心 safety
nonsense.foo();

// 调用者需要为 safety 负责
unsafe { nonsense.bar() };
}

unsafe trait是对trait的实现者的约束,它告诉trait的实现者:实现我的时候要小心,要保证内存安全,所以实现的时候需要加unsafe关键字.

但 unsafe trait 对于调用者来说,可以正常调用,不需要任何 unsafe block,因为这里的 safety 已经被实现者保证了,毕竟如果实现者没保证,调用者也做不了什么来保证 safety,就像我们使用 Send/Sync 一样。

而 unsafe fn 是函数对调用者的约束,它告诉函数的调用者:如果你胡乱使用我,会带来内存安全方面的问题,请妥善使用,所以调用 unsafe fn 时,需要加 unsafe block 提醒别人注意。

调用已有的unsafe函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use std::collections::HashMap;

fn main() {
let map = HashMap::new();
let mut map = explain("empty", map);

map.insert(String::from("a"), 1);
explain("added 1", map);
}

// HashMap 结构有两个 u64 的 RandomState,然后是四个 usize,
// 分别是 bucket_mask, ctrl, growth_left 和 items
// 我们 transmute 打印之后,再 transmute 回去
fn explain<K, V>(name: &str, map: HashMap<K, V>) -> HashMap<K, V> {
let arr: [usize; 6] = unsafe { std::mem::transmute(map) };
println!(
"{}: bucket_mask 0x{:x}, ctrl 0x{:x}, growth_left: {}, items: {}",
name, arr[2], arr[3], arr[4], arr[5]
);

// 因为 std:mem::transmute 是一个 unsafe 函数,所以我们需要 unsafe
unsafe { std::mem::transmute(arr) }
}

对裸指针解引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fn main() {
let mut age = 18;

// 不可变指针
let r1 = &age as *const i32;
// 可变指针
let r2 = &mut age as *mut i32;

// 使用裸指针,可以绕过 immutable / mutable borrow rule

// 然而,对指针解引用需要使用 unsafe
unsafe {
println!("r1: {}, r2: {}", *r1, *r2);
}
}

fn immutable_mutable_cant_coexist() {
let mut age = 18;
let r1 = &age;
// 编译错误
let r2 = &mut age;

println!("r1: {}, r2: {}", *r1, *r2);
}

使用裸指针,可变指针和不可变指针可以共存,不像可变引用和不可变引用无法共存。这是因为裸指针的任何对内存的操作,无论是 ptr::read / ptr::write,还是解引用,都是 unsafe 的操作,所以只要读写内存,裸指针的使用者就需要对内存安全负责。

不可信的内存地址,使用unsafe:

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
// 裸指针指向一个有问题的地址
let r1 = 0xdeadbeef as *mut u32;

println!("so far so good!");

unsafe {
// 程序崩溃
*r1 += 1;
println!("r1: {}", *r1);
}
}

使用FFI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use std::mem::transmute;

fn main() {
let data = unsafe {
let p = libc::malloc(8);
let arr: &mut [u8; 8] = transmute(p);
arr
};

data.copy_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]);

println!("data: {:?}", data);

unsafe { libc::free(transmute(data)) };
}

不推荐使用unsafe的场景

访问或者修改可变静态变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use std::thread;

static mut COUNTER: usize = 1;

fn main() {
let t1 = thread::spawn(move || {
unsafe { COUNTER += 10 };
});

let t2 = thread::spawn(move || {
unsafe { COUNTER *= 10 };
});

t2.join().unwrap();
t1.join().unwrap();

unsafe { println!("COUNTER: {}", COUNTER) };
}

应该改进为AtomicXXX/或者Mutex/RwLock:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use std::{
sync::atomic::{AtomicUsize, Ordering},
thread,
};

static COUNTER: AtomicUsize = AtomicUsize::new(1);

fn main() {
let t1 = thread::spawn(move || {
COUNTER.fetch_add(10, Ordering::SeqCst);
});

let t2 = thread::spawn(move || {
COUNTER
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |v| Some(v * 10))
.unwrap();
});

t2.join().unwrap();
t1.join().unwrap();

println!("COUNTER: {}", COUNTER.load(Ordering::Relaxed));
}

在宏里使用unsafe

在宏中使用 unsafe,是非常危险的。
首先使用你的宏的开发者,可能压根不知道 unsafe 代码的存在;其次,含有 unsafe 代码的宏在被使用到的时候,相当于把 unsafe 代码注入到当前上下文中。在不知情的情况下,开发者到处调用这样的宏,会导致 unsafe 代码充斥在系统的各个角落,不好处理;最后,一旦 unsafe 代码出现问题,你可能都很难找到问题的根本原因。

使用unsafe提升性能

性能,是一个系统级的问题。在你没有解决好架构、设计、算法、网络、存储等其他问题时,就来抠某个函数的实现细节的性能,我认为是不妥的,尤其是试图通过使用 unsafe 代码,跳过一些检查来提升性能。
要知道,好的算法和不好的算法可以有数量级上的性能差异。而有些时候,即便你能够使用 unsafe 让局部性能达到最优,但作为一个整体看的时候,这个局部的优化可能根本没有意义。