目录

📝 摘要

一、Unsafe Rust 的哲学

1.1 为什么需要 Unsafe?

1.2 Unsafe 的含义

二、Unsafe 的五大超能力

2.1 1. 解引用裸指针

2.2 2. 调用 Unsafe 函数或方法

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

2.4 4. 实现 Unsafe Trait

2. 5. 访问联合体(Union)的字段

三、未定义行为(Undefined Behavior, UB)

四、FFI(外部函数接口)实战

4.1 目标:调用 C 语言的 abs 函数

4.2 实战案例:构建 C 语言可调用的 Rust 库

五、安全封装 Unsafe

5.1 封装裸指针

5.2 MaybeUninit

六、Miri:检测 Unsafe 代码的利器

七、总结与讨论

参考链接


📝 摘要

Rust 以其强大的内存安全保证而闻名,但有时我们需要打破这些限制以实现极致性能或与 C 语言库交互。unsafe 关键字是 Rust 提供的“后门”。本文将深入探讨 Unsafe Rust 的使用场景、五大超能力、常见的未定义行为(UB),并通过完整的 FFI(外部函数接口)实战,展示如何在保证安全封装的前提下释放 Rust 的全部底层潜能。


一、Unsafe Rust 的哲学

1.1 为什么需要 Unsafe?

Rust 编译器的安全检查是保守的。它必须在编译期证明代码是 100% 内存安全的。但有些情况下,代码**上**是安全的,但编译器无法证明:

  1. 硬件交互:直接读写内存映射的硬件寄存器。
  2. **性能**:使用 Vec 未初始化的缓冲区进行写操作。
  3. FFI:调用 C、C++ 等其他语言的函数。
  4. 底层数据结构:实现如 VecBTreeMap 等需要精细内存控制的类型。

1.2 Unsafe 的含义

unsafe 关键字不会关闭 Rust 编译器的所有检查。它只“解锁”了五种 Rust 编译器无法保证内存安全的操作。

核心思想:通过 unsafe 块,你向编译器承诺:“**相信我,我已经验证了这段代码的内存安全。**”

安全抽象(Safe Abstraction)
unsafe 的最佳实践是将其封装在安全的 API 之后。Vec::push 内部可能使用 unsafe,但它提供的外部接口是 100% 安全的。

在这里插入图片描述


二、Unsafe 的五大超能力

2.1 1. 解引用裸指针

裸指针(Raw Pointers)分为 *const T(不可变)和 *mut T(可变)。

fn main() {
    let mut num = 5;
    
    // 创建裸指针
    let r1 = &num as *const i32;
    let r2 = &mut num as *mut i32;
    
    // 裸指针可以在安全代码中创建和传递
    let address = 0x012345usize;
    let r3 = address as *const i32;
    
    // 只能在 unsafe 块中解引用
    unsafe {
        println!("r1 (const) 指向: {}", *r1);
        
        *r2 = 10; // 写入
        println!("r2 (mut) 指向: {}", *r2);
        
        // ❌ 危险:r3 可能指向无效内存
        // println!("r3 指向: {}", *r3);
    }
}

2.2 2. 调用 Unsafe 函数或方法

// 示例:Vec 的内部方法
fn main() {
    let mut vec = vec![1, 2, 3];
    
    // 假设我们知道索引是有效的
    unsafe {
        // get_unchecked 不执行边界检查,速度更快
        let value = vec.get_unchecked(1);
        println!("索引1的值 (unchecked): {}", value);
        
        // ❌ 索引越界,导致未定义行为
        // let value = vec.get_unchecked(10);
    }
    
    // 安全版本
    let value = vec.get(1);
    println!("索引1的值 (safe): {:?}", value);
}

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

static mut COUNTER: u32 = 0;

fn add_to_counter(inc: u32) {
    // 访问可变静态变量必须在 unsafe 块中
    unsafe {
        COUNTER += inc;
    }
}

fn main() {
    add_to_counter(3);
    
    // ❌ 多线程访问会导致数据竞争
    std::thread::spawn(|| {
        add_to_counter(5);
    });
    
    unsafe {
        println!("COUNTER: {}", COUNTER);
    }
}

2.4 4. 实现 Unsafe Trait

Send 和 Sync 是最常见的 Unsafe Trait。

// 假设我们有一个包装裸指针的类型
struct MyPointer(*const u8);

// 默认情况下,裸指针不是 Send 或 Sync
// 但如果我们确信在多线程中传递它是安全的
unsafe impl Send for MyPointer {}
unsafe impl Sync for MyPointer {}

fn main() {
    let ptr = MyPointer(0x123 as *const u8);
    
    // 现在可以安全地在线程间移动
    std::thread::spawn(move || {
        println!("在线程中: {:?}", ptr.0);
    });
}

2. 5. 访问联合体(Union)的字段

union MyUnion {
    f1: u32,
    f2: f32,
}

fn main() {
    let mut u = MyUnion { f1: 1_065_353_216 }; // 1.0f32 的 u32 表示
    
    let value = unsafe { u.f1 };
    println!("u32 值: {}", value);
    
    // 访问 f2
    let float_val = unsafe { u.f2 };
    println!("f32 值: {}", float_val);
    
    // 写入
    unsafe {
        u.f2 = 2.0;
        println!("写入2.0后,u32: {}", u.f1);
    }
}

三、未定义行为(Undefined Behavior, UB)

Unsafe 的最大危险在于未定义行为。UB 意味着你的程序可能:

  • 正常工作
  • 崩溃(段错误)
  • 数据损坏
  • 产生安全漏洞
  • 在 release 模式下正常,在 debug 模式下崩溃(反之亦然)

常见 UB 示例

fn main() {
    // 1. 悬垂指针
    let ptr;
    {
        let x = 5;
        ptr = &x as *const i32;
    }
    // unsafe { println!("{}", *ptr); } // ❌ UB: x 已被销毁
    
    // 2. 数组越界
    let slice = &[1, 2, 3];
    // unsafe { println!("{}", *slice.get_unchecked(10)); } // ❌ UB
    
    // 3. 违反引用别名规则
    let mut data = 10;
    let r1 = &mut data as *mut i32;
    let r2 = &mut data as *mut i32;
    // unsafe {
    //     *r1 = 20;
    //     *r2 = 30; // ❌ UB: 多个可变引用别名
    //     println!("{}", *r1);
    // }
    
    // 4. 使用未初始化的内存
    // let x: i32 = unsafe { std::mem::uninitialized() }; // ❌ 已废弃
    let mut x: i32 = unsafe { std::mem::MaybeUninit::uninit().assume_init() };
    // unsafe { println!("{}", x); } // ❌ UB: 读取未初始化的值
}

四、FFI(外部函数接口)实战

FFI 是 unsafe 最主要和最安全的用途之一。

4.1 目标:调用 C 语言的 abs 函数

我们将调用 C 标准库中的 abs 函数(计算整数绝对值)。

1. 声明外部函数

// main.rs
extern "C" {
    // 声明 C 库中的 abs 函数签名
    fn abs(input: i32) -> i32;
}

fn main() {
    let num = -10;
    
    // 调用 C 函数必须在 unsafe 块中
    unsafe {
        let absolute = abs(num);
        println!("{} 的绝对值是 {}", num, absolute);
    }
}

**2链接 C 库**

默认情况下,Rust 会链接 C 标准库(libc),所以 abs 可以直接使用。

cargo run
# 输出:-10 的绝对值是 10

4.2 实战案例:构建 C 语言可调用的 Rust 库

我们将创建一个 Rust 库,计算斐波那契数列,并提供 C 语言 API。

1. 创建 Rust项目

cargo new --lib fibonacci
cd fibonacci

2. 修改 Cargo.toml

# Cargo.toml
[lib]
crate-type = ["cdylib"]  # 生成 C 动态库

**3. 编写 Rust代码**

// src/lib.rs

// Rust 内部的安全实现
fn fib(n: u32) -> u64 {
    match n {
        0 => 0,
        1 => 1,
        _ => fib(n - 1) + fib(n - 2),
    }
}

// 暴露给 C 的 API
#[no_mangle]
pub extern "C" fn fibonacci_calculate(n: u32) -> u64 {
    fib(n)
}

// C 字符串处理示例
use std::ffi::{CStr, CString};
use std::os::raw::c_char;

#[no_mangle]
pub extern "C" fn greet(name: *const c_char) -> *mut c_char {
    // 1. 将 C 字符串转换为 Rust 字符串
    let c_str = unsafe {
        if name.is_null() {
            return CString::new("Error: name is null").unwrap().into_raw();
        }
        CStr::from_ptr(name)
    };
    
    let rust_str = match c_str.to_str() {
        Ok(s) => s,
        Err(_) => return CString::new("Error: Invalid UTF-8").unwrap().into_raw(),
    };
    
    // 2. Rust 逻辑
    let greeting = format!("Hello from Rust, {}! 🦀", rust_str);
    
    // 3. 将 Rust String 转换回 C 字符串
    let c_string = CString::new(greeting).unwrap();
    c_string.into_raw() // 转移所有权给调用者
}

// 释放内存的函数
#[no_mangle]
pub extern "C" fn free_string(s: *mut c_char) {
    if s.is_null() {
        return;
    }
    unsafe {
        // 重新获取所有权并释放
        let _ = CString::from_raw(s);
    }
}

4. 编译 Rust 库

cargo build --release
# 生成:target/release/libfibonacci.so (Linux)
#       target/release/fibonacci.dll (Windows)
#       target/release/libfibonacci.dylib (macOS)

5. 创建 C 语言调用程序

// main.c
#include <stdio.h>
#include <stdint.h>

// 声明 Rust 函数
uint64_t fibonacci_calculate(uint32_t n);
char* greet(const char* name);
void free_string(char* s);

int main() {
    // 1. 调用数字计算
    uint32_t n = 20;
    uint64_t result = fibonacci_calculate(n);
    printf("Rust calculated: fib(%u) = %llu\n", n, result);
    
    // 2. 调用字符串处理
    const char* name = "C Language";
    char* greeting = greet(name);
    
    if (greeting != NULL) {
        printf("Rust says: %s\n", greeting);
        
        // 3. 释放 Rust 返回的字符串
        free_string(greeting);
    }
    
    return 0;
}

**6. 编译并运行 C 程序

# Linux
gcc main.c -L target/release -lfibonacci -o main
export LD_LIBRARY_PATH=./target/release
./main

# macOS
gcc main.c -L target/release -lfibonacci -o main
./main

预期

Rust calculated: fib(20) = 6765
Rust says: Hello from Rust, C Language! 🦀

五、安全封装 Unsafe

5.1 封装裸指针

use std::slice;

// 目标:将 C 数组 (指针 + 长度) 封装为安全 Rust 切片
pub fn slice_from_c_parts<'a>(ptr: *const u8, len: usize) -> Option<&'a [u8]> {
    if ptr.is_null() {
        return None;
    }
    
    // unsafe 块用于验证
    unsafe {
        // 验证 ptr 和 len 是否构成有效的切片
        // ... (此处省略复杂的内存验证)
        
        // 创建安全切片
        Some(slice::from_raw_parts(ptr, len))
    }
}

fn main() {
    let c_array = [10u8, 20, 30, 40];
    let ptr = c_array.as_ptr();
    let len = c_array.len();
    
    if let Some(slice) = slice_from_c_parts(ptr, len) {
        println!("安全切片: {:?}", slice);
    }
}

5.2 MaybeUninit<T>

MaybeUninit<T> 是处理未初始化内存的现代、安全方式。

use std::mem::MaybeUninit;

fn main() {
    // 1. 创建未初始化的数组
    let mut data: [MaybeUninit<i32>; 10] = unsafe {
        MaybeUninit::uninit().assume_init()
    };
    
    // 2. 逐个初始化
    for i in 0..10 {
        data[i] = MaybeUninit::new(i as i32);
    }
    
    // 3. 安全地转换为初始化数组
    // 必须确保所有元素都已初始化
    let initialized_data: [i32; 10] = unsafe {
        std::mem::transmute_copy(&data)
        // 或者 data.map(|d| d.assume_init())
    };
    
    println!("初始化后的数据: {:?}", initialized_data);
}

六、Miri:检测 Unsafe 代码的利器

Miri 是一个实验性的 Rust 解释器,可以检测由 unsafe 引起的多种未定义行为。

安装 Miri

rustup +nightly component add miri

使用 Miri

// src/main.rs
fn main() {
    let mut data = 10;
    let r1 = &mut data as *mut i32;
    let r2 = &mut data as *mut i32;
    
    unsafe {
        *r1 = 20;
        *r2 = 30; // 潜在的 UB
        println!("{}", data);
    }
}
cargo +nightly miri run
# Miri 会报错:
# error: Undefined Behavior: attempting a write access using ...
#        which is stacked borrows conflicting access ...

七、总结与讨论

unsafe 是 Rust 必不可少的一部分,它赋予了 Rust 底层控制能力和 FFI 兼容性。

✅ 核心原则:将 unsafe 限制在最小范围,并将其封装在 100% 安全的 API 之后。
✅ FFIunsafe 是与 C/C++ 库交互的桥梁。
✅ 性能:用于实现 Vec 等零成本抽象。
✅ 工具:使用 Miri 检测 unsafe 代码中的 UB。

讨论问题

  1. 你在项目中是否使用过 unsafe?用它解决了什么问题?
  2. 在编写 FFI 封装时,你认为最容易出错的地方在哪里?
  3. RefCell(运行时检查)和 unsafe(无检查)在内部可变性上如何选择?

欢迎分享你的经验!💬


参考链接

  1. Rust Book - Unsafe Rust:https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html
  2. Rust Nomicon(Unsafe 圣经):[https://doc.rust-lang.org/nomicon/](https://doc.[https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html](https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html)
  3. Miri (UB 检测器):https://github.com/rust-lang/miri
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐