The Rust Programming Language - 第19章 高级特征 - 19.1 不安全的Rust

19 高级特征

我们将在这一章学习更多高级功能

19.1 不安全的Rust

截至目前,我们所编译的代码Rust在编译时会强制执行检查,以确保内存安全。但是Rust还提供了一种模式,不安全Rust,这些代码被编写在 unsafe 块中,它们与常规代码无异,但是能提供额外的功能,满足我们无法通过安全Rust代码实现的需求

为什么要设计不安全Rust呢?原因有二:

1.静态分析本质上比较保守,所以使用安Rust时,编译器在检查一段代码是否支持某个保证时,当它不能确定时,即使这段代码本身是安全的,由于保守规则,它也会拒绝这段代码,这就出现了误杀代码的情况

我们可以在不安全的Rust中编译这些编译器无法确定的代码,但是缺点就是我们得自己负责代码的安全性

2.底层计算机硬件固有的不安全性,如果不允许不安全的Rust,那有些任务我们就无法完成了。Rust需要执行一些操作,像直接与操作系统交互,甚至实现一些底层系统编程,比如像编写我们自己的操作系统。这也是Rust的目标和它的强悍之处

Rust不安全的超能力主要包括以下几个方面

1.解引用裸指针

2.调用不安全的函数或方法

3.访问或修改可变静态变量

4.实现不安全的trait

5.访问union的字段

另外,注意:unsafe并不会关闭借用检查器或者禁用其他的rust检查:所以如果在不安全的代码中使用引用,它仍然会被检查。unsafe关键字只是为上述五个功能提供了不检查的例外。所以我们仍然可以在不安全的块中获得某种程度的安全

还有就是unsafe块中的代码并不一定就代表不安全,原因上面已经讲过

当我们编译了不安全代码,我们希望把它隔离起来。把它封装进一个安全的抽象并提供安全的API是个好方法。这样会防止unsafe泄露

让我们来看看这些功能

解引用裸指针

在常规代码中,编译器总会确保引用是有效的

不安全Rust有两个类似于引用的新类型被称为裸指针。和引用一样,裸指针分为可变的和不可变的嘛,分别写作*const T和 *mut T。这里的 * 是类型的一部分,而不是解引用运算符。不可变意味着解引用后不能直接赋值

裸指针和引用、智能指针的区别

1.允许忽略借用规则,可以同时拥有可变和不可变指针,或多个指向相同位置的可变指针

2.不能保证指向有效的内存

3.允许为空

4.不能实现任何自动清理功能

通过放弃Rust的安全保证,我们可以获得性能或使用另一个语言或硬件接口的能力

fn main() {
    let mut num = 5;

    let r1 = &num as *const i32;
    let r2 = &mut num as *mut i32;

    println!("{:?},{:?}",r1,r2)
}
     Running `target/debug/advancedfunction`
0x7ffeed3519fc,0x7ffeed3519fc

通过引用创建不可变和可变裸指针并打印(直接从保证安全的引用来创建裸指针总是能够保证其是有效的),这里使用as将可变引用和不可变引用强制转为对应的裸指针类型

我们可以不在unsafe代码块中创建裸指针,只是它之外解引用

我们再来创建一个不能确定其有效性的裸指针。尝试使用任意内存是未定义行为,此地址可能有数据也可能没有,编译可能会优化掉这个内存访问,或者程序可能会出现段错误,通常没有好的理由编写这样的代码,不过却是可行的

fn main() {
  let address = 0x012345usize;
  let r = address as *const i32;

  println!("{:?}",r)
}

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

现在我们在unsafe块中解引用裸指针,因为我们没法在不安全块外解引用裸指针

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

    println!("{:?},{:?}",r1,r2);
        
    unsafe {
        println!("r1 is: {}", *r1);
        println!("r2 is: {}", *r2)
    }
}
     Running `target/debug/advancedfunction`
0x7ffeec00695c,0x7ffeec00695c
r1 is: 5
r2 is: 5	

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

虽然通过裸指针我们能够同时创建同一地址的可变指针好的不可变指针,若通过可变指针修改数据,则可能潜在造成数据竞争。请务必注意

那既然有这个危险,为什么还要有这个功能呢?一是我们可以调用c代码接口。另一个是构建借用检查器无法理解的安全抽象

调用不安全函数或方法

类似的,不安全函数和方法与常规的函数和方法也十分类似,除了其前有个unsafe关键字,当然这些函数的本身就需要我们自己负责了

unsafe fn dangerous() {}

unsafe {
    dangerous();
}

必须在unsafe块中调用 unsafe 函数,否则编译器会报错

src/main.rs:14:5
   |
14 |     dangerous()
   |     ^^^^^^^^^^^ call to unsafe function
   |
   = note: consult the function's documentation for information on how to avoid undefined behavior

将函数的调用插入在unsafe块中就表明告知了编译,我们自己知道我们在干什么,不安全的函数体也是有效的unsafe块,所以在其中进行另一个不安全的操作无需新增额外的unsafe块

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

函数中包含部分不安全的代码并不意味着整个函数都是不安全的,我们一般会将不安全代码封装进安全函数,它是一个较常见的抽象

fn main() {
    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]);
}

将一个slice分为两个slice,在这里我们使用了split_at_mut函数

只用安全Rust实现这个函数可能会像如下这样,只适用于i32而非 泛型 T

pub fn split_at_mut(slice: &mut [i32],mid:usize) -> (&mut [i32],&mut [i32]) {
    let len = slice.len();
    assert!(mid <= len);

    (&mut slice[..mid],
     &mut slice[mid..])
}
error[E0499]: cannot borrow `*slice` as mutable more than once at a time
 --> src/main.rs:9:11
  |
4 |   pub fn split_at_mut(slice: &mut [i32],mid:usize) -> (&mut [i32],&mut [i32]) {
  |                              - let's call the lifetime of this reference `'1`
...
8 |       (&mut slice[..mid],
  |       -     ----- first mutable borrow occurs here
  |  _____|
  | |
9 | |      &mut slice[mid..])
  | |___________^^^^^_______- returning this value requires that `*slice` is borrowed for `'1`
  |             |
  |             second mutable borrow occurs here

我们在代码中借用了slice的两个不同的片段,这样操作是安全的,但是Rust还不够智能,它觉得我们借用了同一个slice两次,所以拒绝了这段代码。那我们只能用不安全Rust来实现这个了

use std::slice;

pub 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),)
    }
}

我们来看一下详情

因为我们要将同一个slice借用两次(实际上借用了两个不同的片段),所以我们使用裸指针,as_mut_ptr会将可变引用强转为裸指针

pub const fn as_mut_ptr(&mut self) -> *mut T {
        self as *mut [T] as *mut T
    }

有了裸指针,我们就可以在unsafe代码块中多次使用了,再顺便看看from_raw_parts_mut干了什么,它把slice切成了两半!

pub unsafe fn from_raw_parts_mut<'a, T>(data: *mut T, len: usize) -> &'a mut [T] {
    debug_assert!(is_aligned_and_not_null(data), "attempt to create unaligned or null slice");
    debug_assert!(
        mem::size_of::<T>().saturating_mul(len) <= isize::MAX as usize,
        "attempt to create slice covering at least half the address space"
    );
    unsafe { &mut *ptr::slice_from_raw_parts_mut(data, len) }
}

注意:我们不需要将split_at_mut 函数的结果标记为unsafe,并且可以在安全Rust中调用此函数。这是因为我们创建了一个不安全代码的安全抽象,以一种安全的方式使用了unsafe代码,因为我们从利用参数(参数是有效的)直接创建了有效的裸指针

但是如下函数在使用slice时可能会崩溃

use std::slice;

let address = 0x012345usize;
let r = address as *const i32;

let slice: &[i32] = unsafe {
    slice::from_raw_parts_mut(r,10000)
}

通过任意内存地址创建slice

我们并不拥有这个任意地址的内存,也不能保证这段代码创建的slice包含有效的i32值。试图使用臆测为有效的slice会导致未定义的行为

使用extern 函数调用外部代码

Rust中有个关键字extern可以让我们使用Rust代码与其他语言交互,它有助于创建和使用外部接口。外部函数接口是一个编程语言用以定义函数的方式,其允许不同(外部)编程语言调用这些函数

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

声明并调用另一个语言中的extern函数

extern 块中声明的函数在Rust代码中总是不安全的,因为其他语言不会强制执行Rust的规则且Rust无法检查他们,所以确保其安全是程序员的责任

在extern “C”块中,我们列出了想要调用的另一个语言的函数签名和名称。“C”部分定义了外部函数所使用的 应用二进制接口(ABI),ABI定义了如何在汇编语言层面调用此函数。“C” ABI 是最常见的,并遵循 C编程语言的ABI

从其他语言调用Rust函数

我们也可以使用extern来创建一个允许其他语言调用Rust函数的接口

不同于extern块,就在fn关键字之前增加extern关键字并指定所用到的ABI,还需增加 #[no_mangle]注解来告诉Rust编译器不要mangle此函数的名称。mangle发生于当编译器将我们指定的函数名称修改为不同的名称时,这会增加用于其他编程过程的额外信息,不过会使其名称更加难以阅读。每个编程语言的编译器都会以稍微不同的方式mangle函数名,所以为了使Rust函数能在其他语言中指定,必须禁用Rust编译器的name mangling

如下例子,一旦其编译为动态库并从C语言中链接,call_from_c函数就能够在c代码中访问

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

extern使用无需unsafe

访问或修改可变静态变量

全局变量在Rust中被称为静态变量,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);
    }
}

这种竞争问题,请优先使用并发技术和线程安全智能指针,这样编译器就能检测不同线程间的数据访问是否是安全的

实现不安全的trait

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

unsafe trait Foo {
    //methods go here
}
unsafe impl Foo for i32 {
    //method implementations go here
}

定义并实现不安全的trait

在16章“使用Sync 和 Send trait的可扩展并发“部分中的Sync 和Send标记 trait,编译器会自动为完全由 Send和Sync 类型组成的类型自动实现它们,如果实现包含了像裸指针这样不是Sync和Send类型,并希望将此类型又标记为Send或Sync,则必须使用unsafe

访问联合体中的字段

union和struct相似,但是在一个实例中同时只能使用一个声明的字段

联合体主要用于和C代码中的联合体交互,访问联合体的字段是不安全的,因为Rust无法保证当前存储在联合体实例中的数据的类型,可以查看参考文档了解更多信息

何时使用不安全的代码

当然是在有必要且我们能够保证其安全时使用,毕竟在这几种情况下编译器不能帮助保证内存安全

上一篇:RTOS系统 音频player对比分析


下一篇:Rust单链表