Rust 学习笔记(十三)|Unsafe Rust

1 何时使用 Unsafe Rust

Rust代码本身是安全的,但是有些时候我们不得不使用Unsafe Rust。主要有以下两个原因:

  1. Rust编译器十分保守,很多时候代码实际上没有问题,但是由于编译器可能无法完全静态分析所有的代码,所以编译器就会直接报错;
  2. Rust是一门面向底层的编程语言,而计算机底层硬件本身就是不安全的,所以当使用Rust开发操作系统,或者使用Rust开发嵌入式设备时常常需要使用Rust;
  3. 很多时候,Rust需要和C/C++协同使用,Unsafe可以为Rust提供和C语言的接口。

Unsafe Rust虽然存在一个吓人的“Unsafe”,但是它也并不是完全允许程序员放飞自我,Rust在Unsafe代码中依然存在一定的安全性支撑。例如Unsafe并不能饶过Rust的借用规则,也不能关闭任何的Rust安全性检查。

不需要对Unsafe Rust谈虎色变,Rust标准库中也存在Unsafe代码。没有必要用的时候就不用,有必要用的时候就大胆用,但是要注意Unsafe代码的边界,尽量使得Unsafe代码的范围尽可能小。

2 Unsafe 带来的四大超能力

2.1 解引用裸指针

裸指针(raw pointer,又称为原生指针、原始指针)有点类似C/C++的野指针,它为Rust提供了直接操作内存地址的能力。它也有可变和不可变之分,不可变称为*const T(在解引用之后不能对其进行赋值),可变称为*mut T通过原始指针,我们就可以实现与其他语言的接口,或者实现一些借用检查器无法理解的抽象。与引用不同,裸指针具有以下特性:

  • 可以绕过借用规则,使得数据可以同时拥有一个可变的和不可变的裸指针,甚至可以拥有多个可变的裸指针;
  • 并不保证指向合法内存,甚至可以“捏造”一个空地址;
  • 可以是null;
  • 没有实现任何的自动回收的特性。

在舍弃这些之后,我们就可以获得更好的性能,以及和其他语言或者硬件实现接口的能力。

例如下面的例子,同时为一个数据创建可变的裸指针和不变的裸指针:

fn main() {
    let mut num = 5;

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

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

    unsafe {
        println!("num: {}", *r1);
    }
}
创建裸指针是安全的,但是解引用裸指针是不安全的,解引用裸指针需要将其放在unsafe代码块中。

2.2 调用Unsafe函数或者方法

Unsafe函数或者方法是指在定义前加上了unsafe关键字的函数或者方法。需要在unsafe {}代码块(Unsafe函数也是Unsafe代码块)中调用它们,并且在调用时需要满足该函数/方法需要满足的条件(具体什么条件需要参考文档)。

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

use std::slice;

fn split_at_mid(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 v = vec![1, 2, 3, 4, 5];

    let (a, b) = split_at_mid(&mut v[..], 3);

    assert_eq!(a, vec![1, 2, 3]);
    assert_eq!(b, vec![4, 5]);
}

上面的from_raw_parts_mut函数是一个不安全函数,需要放在Unsafe代码块中,并且split_at_mid函数是一个包裹了不安全代码的安全抽象,所以它可以安全地调用。

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

任何在extern块中的代码都是不安全的,因为Rust不会检查其他语言的安全性。
unsafe extern "C" {     // C指应用二进制接口,ABI(Application Binary Interface)
    fn abs(input: i32) -> i32;
}

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

extern "C"指应用二进制接口,ABI(Application Binary Interface),它定义了函数在汇编层的调用方式,C是最常用的ABI,它指的就是C语言的ABI。

此外,我么还可以使用extern关键字创建一个接口,使得其他语言通过它们可以调用Rust的函数。在函数前除添加extern关键字以外,还需要添加#[no_mangle]注解,避免Rust在编译时改变它的名称。

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

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

Rust支持全局变量,但是因为所有权机制可能产生某些问题,例如数据竞争。Rust中的全局变量称为静态(static)变量。使用static关键字进行声明,使用SCREAMING_SNAKE_CASE命名规范,声明时必须标注类型。静态变量只能存储静态生命周期'static的引用。

静态变量和常量类似,区别在于静态变量的内存地址在程序中是固定的,使用它的值总是会获得相同的数据,而常量在使用时允许对其进行复制。

static HELLO_WORLD: &str = "Hello World";
static mut COUNTER: u32 = 0;

fn add_to_count(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}

fn main() {
    println!("{}", HELLO_WORLD);    // 访问不可变静态变量,安全

    add_to_count(3);

    unsafe {
        // println!("COUNT: {}", COUNTER); // ERROR: shared reference to mutable static in RUST 2024
    }
}

2.4 实现不安全 trait

当某个trait中存在至少一个方法拥有编译器无法校验的不安全因素时,就称这个trait是不安全的。Unsafe trait只能在Unsafe代码块中实现:

unsafe trait Foo {
    // 方法列表
}

unsafe impl Foo for i32 {
    // 实现相应的方法
}

fn main() {}

此外,关于Unsafe Rust还有一些内容,例如使用union实现和C的交互,还有一些社区常用的工具方便多个语言之间的调用等,具体可以参阅Rust语言圣经,在这里不做过多赘述。

转载声明:

除特殊声明外,本站所有文章均由 debussy 原创,均采用 CC BY-NC-SA 4.0 协议,转载请注明出处:Include Everything 的博客
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇