5分钟速读之Rust权威指南(三十九)unsafe

509 阅读5分钟

不安全Rust

因为底层计算机硬件固有的不安全性。如果rust不允许进行不安全的操作,那么某些底层任务可能根本就完成不了,rust拥有”不安全超能力“来与操作系统进行操作,本人的理解rust本身是比C、C++等自由度高的语言多了一层安全的封装,但是呢安全是有条件限制的,很多功能无法实现,此时就需要摆脱这层限制,用“不安全的rust”来实现。

解引用裸指针

不安全Rust的世界里拥有两种类似于引用的新指针类型,它们都被叫作裸指针(raw pointer,有的地方也叫做原生指针),与引用类似,裸指针同样分为两种:

// 不可变裸指针
*const T
// 可变裸指针
*mut T

注意裸指针开头的星号是类型名的一部分而不是解引用操作

裸指针与引用、智能指针的区别:

  • 允许忽略借用规则,可以同时拥有指向同一个内存地址的可变和不可变指针,或者拥有指向同一个地址的多个可变指针。
  • 不能保证自己总是指向了有效的内存地址。
  • 允许为空。
  • 没有实现任何自动清理机制。

创建不可变裸指针

可以使用类型声明和类型转换的方式创建裸指针:

let num = 5;
// 类型声明不可变裸指针
let r1: *const i32 = #

// 使用as操作符将引用转换为裸指针:
let r2 = &num as *const i32;

println!("r1内存地址: {:?}", r1); // r1内存地址: 0x7ffee6b7323c
println!("r2内存地址: {:?}", r2); // r2内存地址: 0x7ffee6b7323c

因为r1``和r2都是通过num```创建的,所以内存地址是一样的。

创建可变裸指针

可变裸指针同上面不可变裸指针差不多,只是把const换成mut

let r1: *mut i32 = &mut num;

// 等价:
let r2 = &mut num as *mut i32;

println!("r1内存地址: {:?}", r1); // r1内存地址: 0x7ffee6b7323c
println!("r2内存地址: {:?}", r2); // r2内存地址: 0x7ffee6b7323c

创建指向任意内存地址的裸指针

我们可以直接将一个数字转为裸指针:

// 0x12345是16进制数,当然也可以写成十进制74565
let address = 0x12345usize;
let r1 = &address as *const usize;
println!("r1内存地址{:?}", r1); // r1内存地址: 0x7ffee6b73380

// 当然可变引用裸引用也是可以的,
// 这里使用十进制i32类型,这里只是想体现用什么数字类型都可以
let mut address2 = 74565i32;
let r2 = &mut address2 as *mut i32;
println!("r2内存地址{:?}", r2); // r2内存地址: 0x7ffee6b73380

裸指针解引用

裸指针支持解引用,用于获取指针对应的真实数据,但是需要使用unsafe块:

let mut num = 5;
let r = &num as *const i32;

// 查看地址
println!("{:?}", r); // 0x7ffee6b733dc

// 解引用获取数据
println!("{}", *r); // 报错,解引用裸指针需要使用unsafe块

// 在unsafe块中解引用裸指针
unsafe {
  println!("{:?}", *r); // 5
}

调用不安全函数或方法

不安全函数定义时前面也需要加unsafe关键字,意味着函数中有不安全的操作:

unsafe fn dangerous() -> i32 {
  let mut num = 5;
  let r = &num as *const i32;
  return *r
}
dangerous(); // 报错,不允许unsafe块外部调用不安全函数

// 在unsafe块中调用
unsafe {
  let num = dangerous();
  println!("{}", num); // 5
}

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

函数中包含不安全代码并不意味着我们需要将整个函数都标记为不安全的。 实际上,将不安全代码封装在安全函数中是一种十分常见的抽象,例如下面使用安全的split_at_mut函数:

let mut v = vec![1, 2, 3, 4, 5, 6];
let r = &mut v[..];
let (a, b) = r.split_at_mut(3);
assert_eq!(a, &mut [1, 2, 3]);
assert_eq!(b, &mut [4, 5, 6]);

上面代码中使用split_at_mut函数将一个数组引用按照索引分割成了数组的两个可变引用,如果在安全的rust中我们是无法实现这个函数的:

fn split_at_mut(slice: &mut [i32], index: usize) -> (&mut [i32], &mut [i32]) {
  let a = &mut slice[..index];
  let b = &mut slice[index..]; // 报错,不能多次利用可变引用借用slice
  return (a, b)
}

上面代码中因为rust不允许我们同时对一个数据创建两个可变引用,所以报错了,此时我们就可以利用unsafe块实现:

fn split_at_mut(slice: &mut [i32], index: usize) -> (&mut [i32], &mut [i32]) {
  // 切片引用由开始位置和长度组成

  // 获取切片的长度
  let len = slice.len();
  
  // 获取切片开始位置的裸指针
  let ptr = slice.as_mut_ptr();

  // 确保分片位置不能大于slice长度
  assert!(index <= len);

  // slice在内存中的开始位置地址
  println!("内存地址: {:?}", ptr); // 内存地址: 0x7ff765c05b20

  unsafe {
    use std::slice;

    // 对slice的开始位置解引用,可以获得第一个元素
    println!("第一个元素: {}", *ptr); // 第一个元素: 6

    // 第一个片段,通过裸指针位置,直接在内存中获取指定长度的切片
    let a = slice::from_raw_parts_mut(ptr, index);
    println!("a: {:?}", a); // a: [6, 5, 4]

    // 第二个片段的开始位置,需要将裸指针指向的内存地址向后移动index位,即:第一个片段的长度
    let move_to_index = ptr.offset(index as isize);
		
    // 通过len - index获取完整切片的剩余长度,作为第二个切片的长度
    let b = slice::from_raw_parts_mut(move_to_index, len - index);
    println!("b: {:?}", b); // b: [3, 2, 1]

    (a, b)
  }
}
let mut v = vec![6, 5, 4, 3, 2, 1];
let r = &mut v[..];
let (a, b) = split_at_mut(r,3);
assert_eq!(a, &mut [6, 5, 4]);
assert_eq!(b, &mut [3, 2, 1]);

上面代码没有将split_at_mut函数标记为unsafe,所以可以在安全Rust中调用该函数。我们创建了一个对不安全代码的安全抽象,并在实现时以安全的方式使用了unsafe代码,因为它仅仅创建了指向访问数据的有效指针,但是有时候from_raw_parts_mut函数有可能导致崩溃:

use std::slice;
let address = 0x12345usize;
let ptr = address as *const i32;
unsafe {
  let data: &[i32] = slice::from_raw_parts(ptr, 10000 as usize); // 报错,无法保证这段代码的切片中一直包含有效的i32值
  println!("data: {:?}", data);
}

因为我们只是拥有内存地址,而不拥有内存数据,所以无法保证其他的变量会使用到这块内存,然后修改了里面的值,导致里边的值不是有效的i32类型,所以编译失败了。

使用extern函数调用外部代码

另外,rust为了与其他语言相互调用,专门提供了extern关键字来简化创建和使用外部函数接口(Foreign Function Interface,FFI)的过程。FFI是编程语言定义函数的一种方式,它允许其他(外部的)编程语言来调用这些函数:

// 声明外部外部函数签名
extern "C" {
  fn abs(input: i32) -> i32;
}

// 需要在unsafe模块中调用
unsafe {
  abs(-10);
}

// 对外向其他语言暴露方法
// 用来注解来避免Rust在编译时改变它的名称
#[no_mangle]
pub extern "C" fn call_from_c () {
  println!("C语言调用了这个方法");
}

访问或修改一个可变静态变量

在rust中,全局变量也被称为静态(static)变量,定义并使用一个不可变静态变量:

static HELLO_WORLD: &str = "hello world";
println!("{}", HELLO_WORLD); // hello world

常量和不可变静态变量看起来可能非常相似,但它们之间存在一个非常微妙的区别: 静态变量的值在内存中拥有固定的地址,使用它的值总是会访问到同样的数据。与之相反的是,常量则允许在任何被使用到的时候复制其数据。

静态变量允许是可变的

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

static mut COUNTER: u32 = 0;

// COUNTER = 1; // 报错,使用或修改都需要unsafe块

unsafe {
  COUNTER = 1;
  println!("{}", COUNTER); // 1
}

实现不安全trait

还有trait,当某个trait中存在至少一个方法拥有编译器无法校验的不安全因素时,我们就称这个trait是不安全的,同样需要使用unsafe来标识这个trait

// 定义和实现一个不安全trait
unsafe trait Foo {
  fn foo() {}
}

// 为i32实现Foo trait
unsafe impl Foo for i32 {
  fn foo() {}
}

封面图:跟着Tina画美国

关注「码生笔谈」公众号,阅读更多最新章节