Rust-裸指针和unsafe

147 阅读5分钟

裸指针

裸指针(raw pointer,又称原生指针) 在功能上跟引用类似,同时它也需要显式地注明可变性。但是又和引用有所不同,裸指针长这样: *const T 和 *mut T,它们分别代表了不可变和可变。在裸指针 *const T 中,这里的 * 只是类型名称的一部分,并没有解引用的含义

特点

  • 可以绕过 Rust 的借用规则,可以同时拥有一个数据的可变、不可变指针,甚至还能拥有多个可变的指针
  • 并不能保证指向合法的内存
  • 可以是 null
  • 没有实现任何自动的回收 (drop)

创建裸指针

基于引用创建裸指针

下面的代码基于值的引用同时创建了可变和不可变的裸指针:

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;

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

基于内存地址创建裸指针

use std::{slice::from_raw_parts, str::from_utf8_unchecked};

// 获取字符串的内存地址和长度
fn get_memory_location() -> (usize, usize) {
  let string = "Hello World!";
  let pointer = string.as_ptr() as usize;
  let length = string.len();
  (pointer, length)
}

// 在指定的内存地址读取字符串
fn get_str_at_location(pointer: usize, length: usize) -> &'static str {
  unsafe { from_utf8_unchecked(from_raw_parts(pointer as *const u8, length)) }
}

fn main() {
  let (pointer, length) = get_memory_location();
  let message = get_str_at_location(pointer, length);
  println!(
    "The {} bytes at 0x{:X} stored: {}",
    length, pointer, message
  );
  // 如果大家想知道为何处理裸指针需要 `unsafe`,可以试着反注释以下代码
  // let message = get_str_at_location(1000, 10);
}

基于智能指针创建裸指针

还有一种创建裸指针的方式,那就是基于智能指针来创建:

let a: Box<i32> = Box::new(10);
// 需要先解引用a
let b: *const i32 = &*a;
// 使用 into_raw 来创建
let c: *const i32 = Box::into_raw(a);

解引用

let a = 1;
let b: *const i32 = &a as *const i32;
let c: *const i32 = &a;
unsafe {
    println!("{}", *c);
}

使用 * 可以对裸指针进行解引用,由于该指针的内存安全性并没有任何保证,因此我们需要使用 unsafe 来包裹解引用的逻辑

api文档

rustwiki.org/zh-CN/std/p…

unsafe

  • unsafe 存在的主要原因是 Rust 的静态检查太强了;Rust 为了内存安全,所做的所有权、借用检查、生命周期等规则往往是普适性的,编译器在分析代码时,一些正确代码会因为编译器无法分析出它的所有正确性,结果将这段代码拒绝,导致编译错误
  • 另一个原因是计算机底层的一些硬件就是不安全的(比如操作 IO 访问外设),这些操作编译器是无法保证内存安全的,如果 Rust 只允许做安全的操作,那就无法完成这些操作,所以需要 unsafe

实现 unsafe trait

任何 trait 只要声明成 unsafe,它就是 unsafe trait。在实现 unsafe trait 时,也必须定义为 unsafe

unsafe trait 是对 trait 的实现者的约束,它表示在实现该 trait 时要小心,要保证内存安全,所以实现时需要加上 unsafe 关键字

但是在调用 unsafe trait 时,直接直接调用,不需要在 unsafe 块中调用,因为这里的安全已经被实现者保证了,毕竟如果实现者没保证,调用者也做不了什么来保证安全,就像使用 Send / Sync trait 一样

// 这是一个unsafe trait,实现这个 trait 的开发者要保证实现是内存安全的
unsafe trait Foo {
    fn foo(&self);
}

struct Nonsense;

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

fn main() {
    let nonsense = Nonsense;
    nonsense.foo();
}

调用 unsafe 函数

使用 unsafe 关键字声明的函数即为 unsafe 函数,一个普通的 trait 里可以包含 unsafe 函数

unsafe fn : 是函数对调用者的约束,它告诉函数的调用者要正确使用该函数,如果乱使用会带来内存安全的问题,所以调用 unsafe fn 时,需要加 unsafe 块把它包裹起来,提醒别人注意这里有 unsafe 代码

另一种调用 unsafe 函数的方法是定义 unsafe fn,然后在这个unsafe fn 里调用其它的 unsafe fn

例如:

trait Bar {
    // 普通的trait里包含 unsafe 函数,表示调用这个函数的人要保证调用是安全的
    unsafe fn bar(&self);
}

struct Nonsense;

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

fn main() {
    let nonsense = Nonsense;

    // 调用者需要为 安全 负责,使用unsafe block包裹起来
    unsafe { nonsense.bar() };
}

解引用裸指针

裸指针的解引用操作是不安全的,有潜在风险,所以解引用时也需要使用 unsafe 来明确告诉编译器,也就是要使用 unsafe 块包裹起来

FFI

最后一种可以使用 unsafe 的地方是 FFI(Foreign Function Interface)。当 Rust 要使用其它语言的能力时(比如 C/C++ 的库),Rust 编译器并不能保证那些语言具备内存安全,所以和第三方语言交互的接口,一律要使用 unsafe

例如,Rust 调用 libc 的 malloc / free 函数时要使用 unsafe 包裹

libc 提供了与 Rust 支持的各平台上的最基础系统 C 库打交道的所有必要设施

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 包裹
    unsafe { libc::free(transmute(data)) };
}

修改静态变脸

静态变量也可以使用mut来将其标注为可变,但是在修改的时候也只能在unsafe块中:

static mut COUNTER: u32= 0;
// COUNTER = 1; // 报错,使用或修改都需要unsafe块
unsafe {
  COUNTER = 1;
  println!("{}", COUNTER); // 1
}