1 何时使用 Unsafe Rust
Rust代码本身是安全的,但是有些时候我们不得不使用Unsafe Rust。主要有以下两个原因:
- Rust编译器十分保守,很多时候代码实际上没有问题,但是由于编译器可能无法完全静态分析所有的代码,所以编译器就会直接报错;
- Rust是一门面向底层的编程语言,而计算机底层硬件本身就是不安全的,所以当使用Rust开发操作系统,或者使用Rust开发嵌入式设备时常常需要使用Rust;
- 很多时候,Rust需要和C/C++协同使用,Unsafe可以为Rust提供和C语言的接口。
Unsafe Rust虽然存在一个吓人的“Unsafe”,但是它也并不是完全允许程序员放飞自我,Rust在Unsafe代码中依然存在一定的安全性支撑。例如Unsafe并不能饶过Rust的借用规则,也不能关闭任何的Rust安全性检查。
2 Unsafe 带来的四大超能力
2.1 解引用裸指针
裸指针(raw pointer,又称为原生指针、原始指针)有点类似C/C++的野指针,它为Rust提供了直接操作内存地址的能力。它也有可变和不可变之分,不可变称为*const T
(在解引用之后不能对其进行赋值),可变称为*mut T
。通过原始指针,我们就可以实现与其他语言的接口,或者实现一些借用检查器无法理解的抽象。与引用不同,裸指针具有以下特性:
- 可以绕过借用规则,使得数据可以同时拥有一个可变的和不可变的裸指针,甚至可以拥有多个可变的裸指针;
- 并不保证指向合法内存,甚至可以“捏造”一个空地址;
- 可以是null;
- 没有实现任何的自动回收的特性。
在舍弃这些之后,我们就可以获得更好的性能,以及和其他语言或者硬件实现接口的能力。
例如下面的例子,同时为一个数据创建可变的裸指针和不变的裸指针:
fn main() {
let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
let address = 0x012345usize;
let r = &address as *const i32;
unsafe {
println!("num: {}", *r1);
}
}
2.2 调用Unsafe函数或者方法
Unsafe函数或者方法是指在定义前加上了unsafe
关键字的函数或者方法。需要在unsafe {}
代码块(Unsafe函数也是Unsafe代码块)中调用它们,并且在调用时需要满足该函数/方法需要满足的条件(具体什么条件需要参考文档)。
函数包含unsafe代码并不意味着需要将整个函数标记为unsafe,将unsafe代码包裹在安全函数中是一个常见的抽象。
use std::slice;
fn split_at_mid(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = slice.len();
let ptr = slice.as_mut_ptr(); // 创建了一个原始指针
assert!(mid <= len);
unsafe {
(
slice::from_raw_parts_mut(ptr, mid),
slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let (a, b) = split_at_mid(&mut v[..], 3);
assert_eq!(a, vec![1, 2, 3]);
assert_eq!(b, vec![4, 5]);
}
上面的from_raw_parts_mut
函数是一个不安全函数,需要放在Unsafe代码块中,并且split_at_mid
函数是一个包裹了不安全代码的安全抽象,所以它可以安全地调用。
extern
关键字可以简化创建和使用外部函数接口(Foreign Function Interface,FFI)的过程。FFI允许一种编程语言定义函数,并让其他编程语言能够调用这些函数。
unsafe extern "C" { // C指应用二进制接口,ABI(Application Binary Interface)
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}
extern "C"
指应用二进制接口,ABI(Application Binary Interface),它定义了函数在汇编层的调用方式,C是最常用的ABI,它指的就是C语言的ABI。
此外,我么还可以使用extern
关键字创建一个接口,使得其他语言通过它们可以调用Rust的函数。在函数前除添加extern
关键字以外,还需要添加#[no_mangle]
注解,避免Rust在编译时改变它的名称。
#[unsafe(no_mangle)]
pub extern fn call_from_c() {
println!("Called a Rust function from C!");
}
2.3 访问或修改一个可变的静态变量
Rust支持全局变量,但是因为所有权机制可能产生某些问题,例如数据竞争。Rust中的全局变量称为静态(static)变量。使用static
关键字进行声明,使用SCREAMING_SNAKE_CASE命名规范,声明时必须标注类型。静态变量只能存储静态生命周期'static
的引用。
静态变量和常量类似,区别在于静态变量的内存地址在程序中是固定的,使用它的值总是会获得相同的数据,而常量在使用时允许对其进行复制。
static HELLO_WORLD: &str = "Hello World";
static mut COUNTER: u32 = 0;
fn add_to_count(inc: u32) {
unsafe {
COUNTER += inc;
}
}
fn main() {
println!("{}", HELLO_WORLD); // 访问不可变静态变量,安全
add_to_count(3);
unsafe {
// println!("COUNT: {}", COUNTER); // ERROR: shared reference to mutable static in RUST 2024
}
}
2.4 实现不安全 trait
当某个trait中存在至少一个方法拥有编译器无法校验的不安全因素时,就称这个trait是不安全的。Unsafe trait只能在Unsafe代码块中实现:
unsafe trait Foo {
// 方法列表
}
unsafe impl Foo for i32 {
// 实现相应的方法
}
fn main() {}
此外,关于Unsafe Rust还有一些内容,例如使用union
实现和C的交互,还有一些社区常用的工具方便多个语言之间的调用等,具体可以参阅Rust语言圣经,在这里不做过多赘述。