
17 不安全rust
有时代码可能是合法的,但如果 Rust 编译器没有足够的信息来确定,它将拒绝该代码。在这种情况下,可以使用不安全代码告诉编译器,“相信我,我知道自己在干什么。”
unsafe块
- 解引用裸指针 (创建裸指针不会有任何不安全)
- 调用不安全的函数或方法
- 访问或修改可变静态变量
- 实现不安全 trait
- 访问
union的字段
为了尽可能隔离不安全代码,最好将不安全代码封装进一个安全的抽象并提供安全 API
裸指针
[!summary] 裸指针类型标识* const与* mut
*const T表示T类型的不可变裸指针类型。不可变指不能通过这个指针修改内部背后代表的数据。如果要指针变量本身不可变,利用let xxx:*const T = xxx
*mut T表示T类型 T类型的可变指针类型。
let mut num = 5;
let r1 = &raw const num;
let r2 = &raw mut num;
注意这段代码中没有引入 unsafe 关键字。可以在安全代码中创建裸指针;只是不能在不安全块之外解引用裸指针。
不安全函数
不安全函数,编译器会确保调用不安全函数需要在unsafe块中。
在不安全函数的函数体内部执行不安全操作时,同样需要使用unsafe 块,就像在普通函数中一样。这有助于将 unsafe 块保持得尽可能小,因为不安全操作未必需要覆盖整个函数体。
将不安全代码封装进安全函数是一种常见的抽象方式。
本质上借用 slice 的不同部分是可以的,因为这两段 slice 不会重叠,不过 Rust 还没有智能到能够理解这些。当我们知道某些事是可以的而 Rust 不知道的时候,就是触及不安全代码的时候了
use std::slice;
fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = values.len();
let ptr = values.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方法来获得切片头裸指针。
提供了from_raw_parts_mut函数来通过裸指针与长度来组成切片引用。
注意无需将split_at_mut 函数的结果标记为 unsafe,并可以在安全 Rust 中调用此函数。我们创建了一个不安全代码的安全抽象。
(通过观察代码,和增加 mid 必然小于等于 len 的断言创造的逻辑安全)
extern函数调用外部代码
有时你的 Rust 代码可能需要与其他语言编写的代码交互。为此 Rust 有一个关键字,extern,有助于创建和使用 外部函数接口(Foreign Function Interface,FFI)。
extern 块中声明的函数在 Rust 代码中通常是不安全的因此 ==extern 块本身也必须标注 unsafe。==之所以如此,是因为其他语言不会强制执行 Rust 的规则,Rust 也无法检查这些约束,因此程序员有责任确保调用的安全性。
在unsafe extern "C" 块中,我们列出了希望能够调用的另一个语言中的外部函数的签名和名称。"C" 部分定义了外部函数所使用的 应用二进制接口(application binary interface,ABI) —— ABI 定义了如何在汇编语言层面调用此函数。
"C" ABI 是最常见的,并遵循 C 编程语言的 ABI。有关 Rust 支持的所有 ABI 的信息请参见 the Rust Reference。
unsafe extern "C" {
safe fn abs(input: i32) -> i32;
}
fn main() {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
unsafe extern 中声明的任何项都隐式地是 unsafe 的。然而,一些 FFI 函数可以安全地调用。我们可以使用 safe 关键字来表明这个特定的函数,一旦我们做出这个修改,调用它不再需要 unsafe 块。(safe关键字) 确保履行这个承诺仍然是你的责任!
在fn 关键字之前增加 extern 关键字并为相关函数指定所用到的 ABI。还需增加 #[no_mangle] 注解来告诉 Rust 编译器不要 mangle (编译时将名字改成编译器喜欢的) 此函数的名称。
#[unsafe(no_mangle)]
pub extern "C" fn call_from_c() {
println!("Just called a Rust function from C!");
}
访问或修改静态变量
这对于 Rust 的所有权规则来说是有问题的。如果有两个线程访问相同的可变全局变量,则可能会造成数据竞争。
访问不可变静态变量是安全的。访问和修改可变静态变量都是不安全 的。
静态变量只能储存拥有'static 生命周期的引用,这意味着 Rust 编译器可以自己计算出其生命周期而无需显式标注。访问不可变静态变量是安全的。
[!todo ] 字符串字面值:它不是栈上变量,也不是堆上变量,而是被硬编码在编译后的可执行文件的只读数据段
运行时内存结构
因为访问可变全局变量是不安全的,所以我们需要在unsafe块中访问。
而定义全局变量很简单,static mut STATIC_NAME = 0;或者没有 mut。
每当我们编写一个不安全函数,惯常做法是编写一个以SAFETY 开头的注释并解释调用者需要做什么才可以安全地调用该方法。同理,当我们进行不安全操作时,惯常做法是编写一个以 SAFETY 开头并解释安全性规则是如何维护的。
只能通过用裸指针解引用操作符创建的裸指针访问它。这包括引用的创建是不可见的情况。也就是你只是对静态变量的表示也只能通过裸指针组装,*(&raw mut XXX)
实现不安全trait
也就是trait的运行,依赖某个承诺,这个承诺只能由各位trait实现者自行实现。如果不满足这个承诺,那么trait将不会按照预期设计的那样运行。
不安全Trait中的不变式,是Trait作者对实现者提出的“硬性内存安全承诺”。它不是语言特性,而是人为约定。
- Trait定义者(作者):在文档中写明不变式,并编写
unsafe函数依赖于该不变式(不做运行时检查)。 - Trait实现者(你):必须在
unsafe impl块中明确承诺自己满足了所有不变式。如果依赖外部数据(如C库),需要手动校验。
// 伪代码示例:假设存在一个不安全Trait要求 ptr 永远非空
unsafe trait NonNullPtr {
fn get_ptr(&self) -> *const u8;
}
struct MyStruct {
ptr: *const u8,
}
// 实现者必须手动保证 ptr 不为空
unsafe impl NonNullPtr for MyStruct {
fn get_ptr(&self) -> *const u8 {
self.ptr // 如果此时 ptr 是 std::ptr::null(),就破坏了不变式!
}
}
访问联合体中的字段
union 和 struct 类似,但是在一个实例中同时只能使用一个已声明的字段。联合体主要用于和 C 代码中的联合体进行交互。访问联合体的字段是不安全的,因为 Rust 无法保证当前存储在联合体实例中数据的类型。
最核心的区别在于:枚举是带“标签”的安全类型,而联合体是不带“标签”的裸内存块。
你无法区别一个联合体中的数据是哪个类型,而枚举则包含了这个标签来进行区分。
使用 Miri检查不安全代码
它通过运行程序,或者测试集来检查代码,并检测你是否违反了它理解的 Rust 应该如何工作的规则。
使用 Miri 要求使用 nightly 版本的 Rust,你可以通过输入rustup +nightly component add miri 来同时安装 nightly 版本的 Rust 和 Miri。这并不会改变你项目正在使用的 Rust 版本;它只是为你的系统增加了这个工具所以你可以在需要的时候使用它。
通过输入 cargo +nightly miri run or cargo +nightly miri test 在项目中使用 Miri。
