Skip to content

Day 28: unsafe Rust

Rust的核心承诺是内存安全——通过所有权、借用检查和生命周期,在编译期消除大部分内存安全问题。但有些操作本质上是不安全的,如:

  • 与C语言代码互操作(FFI)
  • 访问硬件底层
  • 实现其他unsafe trait
  • 操作裸指针

Rust通过unsafe关键字标记这些不安全操作,允许开发者在受控范围内绕过安全检查。

unsafe Rust相比普通Rust多了五个”超能力”:

  1. 解引用裸指针
  2. 调用不安全的函数或方法
  3. 访问或修改可变静态变量
  4. 实现unsafe trait
  5. 访问union的字段
fn main() {
// 1. 解引用裸指针 - 可以在safe代码中进行
let raw_ptr = 5 as *const i32;
unsafe {
println!("裸指针解引用: {}", *raw_ptr);
}
}

注意:裸指针的创建在safe代码中也可以,但解引用必须在unsafe块中。

unsafe函数要求调用者显式处理安全性:

// 这个函数标记为unsafe,调用者必须验证安全性
unsafe fn dangerous_operation(data: *mut i32) -> i32 {
unsafe {
*data // 解引用指针
}
}
fn main() {
let mut value = 42;
// 调用unsafe函数需要unsafe块
unsafe {
let result = dangerous_operation(&mut value as *mut i32);
println!("结果: {}", result);
}
}

unsafe函数是一种契约:函数本身无法保证安全性,调用者必须自己确保前提条件。

裸指针有不可变的*const T和可变的*mut T两种:

fn main() {
let mut value = 10;
// 从引用创建裸指针
let const_ptr: *const i32 = &value;
let mut_ptr: *mut i32 = &mut value;
println!("const_ptr points to: {}", unsafe { *const_ptr });
println!("mut_ptr points to: {}", unsafe { *mut_ptr });
// 可以创建指向任意地址的指针(模拟硬件访问)
let fake_ptr = 0x1000 as *mut i32;
// unsafe { *fake_ptr = 0; } // 取消注释会导致未定义行为
}

Rust能与C库无缝互操作,通过FFI(Foreign Function Interface)调用C函数:

// 声明外部C函数
extern "C" {
fn abs(x: i32) -> i32;
// 假设有一个C函数
// int calculate(int x, int y);
fn calculate(x: i32, y: i32) -> i32;
}
#[repr(C)]
struct Point {
x: f64,
y: f64,
}
fn main() {
// 调用C标准库的abs函数
unsafe {
println!("abs(-5) = {}", abs(-5));
println!("calculate(10, 20) = {}", calculate(10, 20));
}
// 创建C兼容结构体
let p = Point { x: 1.0, y: 2.0 };
println!("C结构体大小: {}", std::mem::size_of::<Point>());
}

extern块声明了外部C函数,#[repr(C)]确保结构体内存布局与C兼容。

除了调用C代码,Rust也可以导出函数给C调用:

#![allow(dead_code)]
// 标记为C ABI
#[no_mangle]
pub extern "C" fn rust_function(x: i32) -> i32 {
x * 2
}
// 导出的回调函数
#[allow(dead_code)]
pub extern "C" fn callback_from_c(data: *const i32, len: usize) -> i32 {
unsafe {
let slice = std::slice::from_raw_parts(data, len);
slice.iter().sum()
}
}
// 从C的角度看,这些函数的ABI是兼容的

#[no_mangle]防止编译器改名,extern "C"指定C ABI。

某些trait的实现是不安全的,因为它们要求实现者保证某些不变性:

unsafe trait UnsafeTrait {
fn method(&self) -> i32;
}
// 实现unsafe trait也需要unsafe块
unsafe impl UnsafeTrait for i32 {
fn method(&self) -> i32 {
*self
}
}

标准库中的Send和Sync就是unsafe trait的例子——它们表示类型可以安全地在线程间传递/访问。

在Rust中访问可变静态变量需要unsafe:

static mut COUNTER: i32 = 0;
fn increment() {
unsafe {
COUNTER += 1;
println!("计数器: {}", COUNTER);
}
}
fn main() {
increment();
increment();
increment();
unsafe {
println!("最终值: {}", COUNTER);
}
}

可变静态变量的访问需要unsafe,因为多线程并发访问会导致数据竞争。通常更好的做法是使用Mutex。

Rust的union访问也需要unsafe:

#[repr(C)]
union IntOrFloat {
as_int: i32,
as_float: f32,
}
fn main() {
let mut u = IntOrFloat { as_int: 42 };
unsafe {
println!("作为i32: {}", u.as_int);
u.as_float = 3.14;
println!("作为f32: {}", u.as_float);
println!("作为i32(重新解释): {}", u.as_int);
}
}

Rust的union主要用于FFI场景,因为标准库已经有Option和Result来处理可选值。

unsafe Rust是Rust类型系统的”出口”,让我们能够在必要场景下进行底层操作:

  1. unsafe块 解引用裸指针
  2. unsafe函数 标记需要调用者保证安全性
  3. FFI 与C代码互操作
  4. unsafe trait 标记需要实现者保证安全性的trait
  5. 可变静态变量 访问需要unsafe(但应避免)

使用unsafe的原则:隔离和最小化。将unsafe代码封装在安全的接口后,让大部分代码保持在safe Rust的安全保障内。