31_rust_高级特性

发布时间 2023-11-30 00:50:36作者: 00lab

高级特性

  • 不安全rust
  • 高级Trait
  • 高级类型
  • 高级函数和闭包

不安全rust

隐藏这第二个语言,其未强制内存安全保证:Unsafe rust(不安全的rust);其和普通rust一样,但提供了额外的“超能力”。
unsafe rust存在的原因:

  • 静态分析是保守的,使用unsafe rust,表示开发者知道自己在做什么,并承担相应风险。
  • 计算机硬件本身就不安全,rust需要能够进行底层系统编程。

unsafe的超能力:
使用unsafe关键字来切换到unsafe rust,开启一个块,块内为unsafe代码。
unsafe rust里可执行四个动作(超能力):

  • 解引用原始指针
  • 调用unsafe函数或方法
  • 访问或修改可变的静态变量
  • 实现unsafe trait

注意:

  • unsafe并未关闭借用检查或停用其他安全检查,如果在里边使用引用,依然会被检查。所以即便在unsafe代码块中依然可获得一定的安全性。
  • 任何内存安全相关的错误必须保留在unsafe块里。
  • 尽可能隔离unsafe代码,最好将其封装在安全的抽象里,提供安全的API。
    很多标准库里也含有unsafe代码,但使用安全接口封装,防止不安全代码泄露到调用处。

解引用原始指针

原始指针分类:

  • 可变的:*mut T
  • 不可变的:*constT。意味着指针在解引用后不能直接对其进行赋值

注意:这里的*不是解引用符号,它是类型名的一部分。

与引用的区别点,原始指针:

  • 允许通过同时具有不可变和可变指针,或多个指向同一位置的可变指针,并忽略借用规则
  • 无法保证能指向合理的内存
  • 允许为 null
  • 不实现任何自动清理
  • 放弃保证的安全,换取更好的性能/与其它语言或硬件接口的能力
fn main() {
  let mut n = 5;
  let r1 = &n as *const i32; // 不可变原始指针
  let r2 = &mut n as *mut i32; // 可变原始指针
  // 可在非安全代码块外创建原始指针,但只能在不安全代码块内进行解引用
  // 在非安全代码块内解引用这两个必定有效的指针
  unsafe {
    println!("r1 {}", *r1);
    println!("r2 {}", *r2);
  }
  // 创建一个无法确定有效性的原始指针
  let addr = 0x0123456usize; // 一个内存地址,可能有数据也可能无
  let r = addr as *const i32; // 创建一个该地址的原始指针,编译不会报错
  unsafe {
    println!("r {}", *r); // 解引用不确定指针,编译不报错,需开发者自己保证安全性
  }
}
/* 运行输出:
r1 5
r2 5
thread 'main' panicked at 'misaligned pointer dereference: address must be a multiple of 0x4 but is 0x123456', src\main.rs:15:7
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
thread caused non-unwinding panic. aborting.
*/

用原始指针的作用:与c语言进行接口,构建借用检查器无法理解的安全抽象。

调用unsafe函数或方法

unsafe函数或方法,在定义前加上unsafe关键字。

  • 调用前需开发者确认满足要求或条件(通常由提供方提供文档说明),因为rust无法对这些条件进行验证。
  • 需要在unsafe块里进行调用。
unsafe fn danger() {}
fn main() {
    unsafe {
        danger(); //正确调用
    }
    danger(); // 编译报错 call to unsafe function is unsafe and requires unsafe function or block
}

创建unsafe代码的安全抽象

函数包含unsafe代码并不意味着需要将整个函数标记为unsafe,将unsafe代码包裹在安全函数中是一个常见的抽象。

fn main() {
    let mut v = vec![1, 2, 3, 4, 5];
    let r = &mut v[..]; // 建立一个完整切片
    // split_at_mut函数是标准库的函数,的作用是根据传入的数字切分vector,
    // 分割成两个切片,分别是1 2 3, 4 5两组
    let (a, b) = r.split_at_mut(3);
    assert_eq!(a, &mut [1, 2, 3]);
    assert_eq!(b, &mut [4, 5]);
}

split_at_mut是库函数,使用了不安全函数,为了理解,这里进行手动定义如下:

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..]) // 试图返回两个切片,以mid作为切分点
    // 但这里会报无法对切片进行两次借用的错误,因为这是rust语言的规则
    // 但实际情况是可以的切分借用的,因为这里借用的是前后两个无交叉部分,切分后无关联,但rust规则限定,无法识别这种情况
}

fn main() {
    let mut v = vec![1, 2, 3, 4, 5];
    let r = &mut v[..]; // 建立一个完整切片
    // split_at_mut函数是标准库的函数,的作用是根据传入的数字切分vector,
    // 分割成两个切片,分别是1 2 3, 4 5两组
    let (a, b) = r.split_at_mut(3);
    assert_eq!(a, &mut [1, 2, 3]);
    assert_eq!(b, &mut [4, 5]);
}

编译报错:cannot borrow *slice as mutable more than once at a time,所以这里需要使用unsafe代码块来实现,进行安全抽象,代码如下:

fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = slice.len();
    let ptr = slice.as_mut_ptr(); //返回原始指针,类型是*mut i32,指向slice
    assert!(mid <= len);
    unsafe {
        (slice::from_raw_parts_mut(ptr, mid), // 创建切片
        slice::from_raw_parts_mut(ptr.add(mid), len - mid))
    } //由于使用了原始指针和偏移量,代码块不安全,调用需要在unsafe块里完成
    // 虽然split_at_mut调用了不安全代码,但自身未标记为不安全函数,它就是不安全代码的安全抽象
}

fn main() {
    let mut v = vec![1, 2, 3, 4, 5];
    let r = &mut v[..]; // 建立一个完整切片
    // split_at_mut函数是标准库的函数,的作用是根据传入的数字切分vector,
    // 分割成两个切片,分别是1 2 3, 4 5两组
    let (a, b) = r.split_at_mut(3);
    assert_eq!(a, &mut [1, 2, 3]);
    assert_eq!(b, &mut [4, 5]);
}

如果在main中不是调用安全抽象函数,而是直接调用非安全函数:

fn main() {
    let addr = 0x0123456usize;
    let r = addr as *const i32;
    let slice: &[i32] = unsafe {
        slice::from_raw_parts_mut(r, 50);
    };
    // 如果这种方式调用,编译不会报错,但运行时可能崩溃,因为无法保障切片是有效的
}

使用extern函数调用外部代码

extern关键字:简化创建和使用外部函数接口(FFI)的过程
外部函数接口(FFI,Foreign Function Interface):允许一种编程语言定义函数,并让其它编程语言能调用这些函数。

extern "C" {
  fn abs(in: i32) -> i32;
}
fn main() {
  unsafe {
    println!("call c abs:{}", abs(-3));
  }
}

应用二进制接口(ABI,Application Binary Interface):定义函数在汇编层的调用方式。
“C” ABI是最常见的ABI,它遵循C语言的ABI。

从其它语言调用rust函数

可使用extern创建接口,其它语言通过它们可调用rust的函数。

  • 在fn前添加extern关键字,并指定ABI
  • 还需添加#[no_mangle]注解(这是一个编译阶段),避免rust在编译时改变它的名称
#[no_mangle]
pub extern "C" fn call_func() {
  println!("func in rust");
}

且无需使用unsafe。

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

rust支持全局变量,但因所有权机制可能产生某些问题,比如数据竞争。
在rust里,全局变量叫静态(static)变量。声明时必须指明类型。其生命周期能被编译器推断出,只存储'static的引用。

static HELLO: &str = "hello";
fn main() {
  println!("{}", HELLO);
}

静态变量

  • 静态变量与常量类似
  • 命名:SCREAMIN_SNAKE_CASE,全大写+下划线分割
  • 必须标注类型
  • 静态变量只能存储'static生命周期的引用,无需显示标注
  • 访问不可变静态变量是安全的

常量和不可变静态变量的区别

  • 静态变量有固定的内存地址,使用它的值总会访问同样的数据
  • 常量允许使用它们的时候对数据进行复制
  • 静态变量可以是可变的,访问和修改静态可变变量是不安全的(unsafe)