Rust在编译时会强制执行内存安全保证。但是Rust还有第二种语言,不会强制执行这类内存安全保证:被称为unsafe rust

之所以需要unsafe rust,是因为有时代码可能是合法的,但是Rust编译器没有足够的信息来确定,那么就会拒绝该代码。还有一部分原因在于:底层计算机硬件固有的不安全性,如果rust不允许进行不安全操作,那么有些任务根本无法完成。

unsafe的超能力

使用unsafe关键字切换到unsafe rust。在unsafe rust中有五类不能再安全rust中进行的操作,称为为unsafe超能力。

  • 解引用裸指针。
  • 调用不安全方法或者函数。
  • 访问或者修改可变静态变量。
  • 实现不安全trait。
  • 访问union字段。

Note

有一点很重要,unsafe 并不会关闭借用检查器或禁用任何其他 Rust 安全检查:如果在不安全代码中使用引用,它仍会被检查。unsafe 关键字只是提供了那五个不会被编译器检查内存安全的功能。你仍然能在不安全块中获得某种程度的安全。

为了尽可能隔离不安全代码,将不安全代码封装进一个安全的抽象并提供安全 API 是一个好主意.

解引用裸指针

不安全 Rust 有两个被称为 裸指针raw pointers)的类似于引用的新类型。和引用一样,裸指针是不可变或可变的,分别写作 *const T 和 *mut T。这里的星号不是解引用运算符;它是类型名称的一部分。在裸指针的上下文中,不可变 意味着指针解引用之后不能直接赋值。

裸指针与c语言中普通的指针行为一致。通过去掉Rust强加的保证,可以放弃安全性保证以换取性能或使用另一个语言或者硬件接口的能力。

fn main() {
    let mut num = 5;
 
    let r1 = &num as *const i32;
    let r2 = &mut num as *mut i32;
}

在安全代码中可以创建裸指针,只是不能在不安全块之外解引用

在unsafe中解引用裸指针

fn main() {
    let mut num = 5;
 
    let r1 = &num as *const i32;
    let r2 = &mut num as *mut i32;
 
    unsafe {
        println!("r1 is: {}", *r1);
        println!("r2 is: {}", *r2);
    }
}
 

创建一个指针不会造成任何危险;只有当访问其指向的值时才有可能遇到无效的值

调用不安全函数或方法

不安全方法是通过unsafe关键字修饰过的。只能在unsafe块中调用。

fn main() {
    unsafe fn dangerous() {}
 
    unsafe {
        dangerous();
    }
}
 

不安全函数体也是有效的 unsafe 块,所以在不安全函数中进行另一个不安全操作时无需新增额外的 unsafe 块

创建不安全代码的安全抽象

函数包含不安全代码并不代表函数是不安全的。实际上,将不安全代码封装进安全函数是常见的抽象。 例如标准库的函数split_at_mut,它是一个安全函数,但是它的实现需要不安全代码、

use std::slice;
 
fn split_at_mut(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 vector = vec![1, 2, 3, 4, 5, 6];
    let (left, right) = split_at_mut(&mut vector, 3);
}

使用extern函数调用外部代码

Rust的extern关键字有助于创建和使用外部函数接口FFI。外部函数接口是一个编程语言用于定义函数的方式,允许不同的编程语言调用这些函数。

调用C语言的abs函数的例子如下:

extern "C" {
    fn abs(input: i32) -> i32;
}
 
fn main() {
    unsafe {
        println!("Absolute value of -3 according to C: {}", abs(-3));
    }
}

extern “C”块中式希望能够调用的另一个语言中的外部函数的签名和名称。 “C”式外部函数使用的应用二进制接口ABI——ABI定义了如何在汇编语言层面调用此函数。“C”ABI式是最常见的,遵循C编程语言的ABI。

如果Rust提供函数给其它语言调用,除了使用extern外还要使用#[no_mangle]注释,这是要让Rust编译器不要mangle此函数的名称,以便其它语言能够识别到。下面的例子编译成动态库并链接到c语言后,C代码就能够访问到call_from_c函数。

#![allow(unused)]
fn main() {
#[no_mangle]
pub extern "C" fn call_from_c() {
    println!("Just called a Rust function from C!");
}
 

访问或修改可变静态变量

全局变量在Rust中被称为静态变量,例如

static HELLO_WORLD: &str = "Hello, world!";
 
fn main() {
    println!("name is: {}", HELLO_WORLD);
}
 

常量在用到的时候复制其值,使用静态变量总是访问相同的地址。 常量是不可变的,静态变量是可变的。

访问和修改可变静态变量是不安全的

static mut COUNTER: u32 = 0;
 
fn add_to_count(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}
 
fn main() {
    add_to_count(3);
 
    unsafe {
        println!("COUNTER: {}", COUNTER);
    }
}

拥有可以全局访问的可变数据,难以保证不存在数据竞争,这就是为何 Rust 认为可变静态变量是不安全的

实现不安全trait

当 trait 中至少有一个方法中包含编译器无法验证的不变式(invariant)时 trait 是不安全的。可以在 trait 之前增加 unsafe 关键字将 trait 声明为 unsafe,同时 trait 的实现也必须标记为 unsafe,例如:

unsafe trait Foo {
    // methods go here
}
 
unsafe impl Foo for i32 {
    // method implementations go here
}
 
fn main() {}
 

比如Sync和Send trait扩展并发中的 Sync 和 Send 标记 trait,编译器会自动为完全由 Send 和 Sync 类型组成的类型自动实现他们。如果实现了一个包含一些不是 Send 或 Sync 的类型,比如裸指针,并希望将此类型标记为 Send 或 Sync,则必须使用 unsafe。Rust 不能验证我们的类型保证可以安全的跨线程发送或在多线程间访问,所以需要我们自己进行检查并通过 unsafe 表明。

访问联合体中的字段

union 和 struct 类似,但是在一个实例中同时只能使用一个声明的字段。联合体主要用于和 C 代码中的联合体交互。访问联合体的字段是不安全的,因为 Rust 无法保证当前存储在联合体实例中数据的类型。