unsafe Rust 与 FFI¶
📋 章节信息¶
| 属性 | 详情 |
|---|---|
| 学习时间 | 6-8小时 |
| 难度等级 | ⭐⭐⭐⭐⭐ 高级 |
| 前置知识 | 所有权与借用、智能指针、生命周期、类型系统 |
| 核心主题 | unsafe代码、裸指针、FFI、安全抽象、UB检测 |
1. unsafe 的五种超能力¶
Rust 的安全保证在编译期检查,但有些操作编译器无法验证其安全性。unsafe 关键字提供了五种额外能力:
- 解引用裸指针(Raw Pointer Dereference)
- 调用 unsafe 函数或方法
- 访问或修改可变静态变量
- 实现 unsafe trait
- 访问
union的字段(因为编译器无法确定当前存储的是哪个变体)
⚠️
unsafe不意味着「这段代码是危险的」,而是告诉编译器:「我已经手动验证了安全性,由我来承担责任。」
// 第5种能力示例:访问 union 字段
union IntOrFloat {
i: i32,
f: f32,
}
fn main() {
let u = IntOrFloat { i: 42 };
// 读取 union 字段需要 unsafe,因为编译器不知道当前存储的是哪个变体
// 不变量:调用者必须保证读取的字段与最后写入的字段类型匹配
let val = unsafe { u.i };
println!("值: {}", val);
}
1.1 unsafe 块的基本语法¶
fn main() {
// 安全代码
let x = 42;
// unsafe 块:开启超能力
unsafe {
// 在这里可以使用四种额外能力
// 其他安全操作在unsafe块中仍然受编译器检查
}
// 回到安全代码
println!("x = {}", x);
}
1.2 unsafe 的边界¶
// unsafe 函数:调用者需要保证前置条件
unsafe fn dangerous() {
// 函数体中的所有代码都处于 unsafe 上下文
println!("这是一个 unsafe 函数");
}
fn main() {
// 必须在 unsafe 块中调用 unsafe 函数
unsafe {
dangerous();
}
}
2. 何时使用 unsafe(必要性分析)¶
2.1 合理使用 unsafe 的场景¶
// ✅ 场景1:与C库交互(FFI)
extern "C" {
fn abs(input: i32) -> i32;
}
fn safe_abs(x: i32) -> i32 {
unsafe { abs(x) } // 必须用 unsafe 调用外部函数
}
// ✅ 场景2:性能关键路径,跳过边界检查
fn get_unchecked_example(slice: &[i32], index: usize) -> i32 {
assert!(index < slice.len()); // 手动验证安全性
unsafe { *slice.get_unchecked(index) }
}
// ✅ 场景3:实现底层数据结构
struct MyVec<T> {
ptr: *mut T,
len: usize,
cap: usize,
}
// ✅ 场景4:内联汇编或硬件访问
fn read_cpu_id() -> u64 {
let result: u64;
unsafe {
std::arch::asm!(
"mov {}, 0",
out(reg) result,
);
}
result
}
2.2 不应使用 unsafe 的场景¶
// ❌ 错误:仅为了绕过借用检查器
fn bad_example() {
let mut data = vec![1, 2, 3];
let ptr = data.as_mut_ptr();
// 不应该通过裸指针绕过借用规则
// unsafe { *ptr = 10; } // 这里有更好的安全替代方案
data[0] = 10; // ✅ 正确做法
}
// ❌ 错误:仅为了避免 Option/Result 处理
fn bad_unwrap(val: Option<i32>) -> i32 {
// unsafe { val.unwrap_unchecked() } // 不要这样做
val.unwrap_or(0) // ✅ 正确做法
}
// ❌ 错误:用transmute转换不相关类型
fn bad_transmute() {
// let x: f64 = unsafe { std::mem::transmute(42u64) }; // 避免这样做
}
2.3 unsafe 使用原则¶
/// 使用 unsafe 的黄金准则:
///
/// 1. 最小化 unsafe 代码范围
/// 2. 在 unsafe 块外提供安全 API
/// 3. 文档化所有安全性不变量(safety invariants)
/// 4. 使用 #[deny(unsafe_op_in_unsafe_fn)] 限制隐式 unsafe
/// 5. 为 unsafe 代码编写充分的测试
// 示例:使用属性强制在 unsafe 函数内显式标注 unsafe 操作
#[deny(unsafe_op_in_unsafe_fn)]
unsafe fn careful_function(ptr: *const i32) -> i32 {
// 即使在 unsafe fn 内,也需要显式 unsafe 块
// 这样能更精确地标识哪些操作是 unsafe 的
unsafe { *ptr }
}
3. 裸指针操作¶
3.1 裸指针基础(*const T / *mut T)¶
fn main() {
let x = 42;
let y = &x as *const i32; // 不可变裸指针
let mut z = 10;
let w = &mut z as *mut i32; // 可变裸指针
// 创建裸指针是安全的,解引用是 unsafe 的
println!("y 指针地址: {:?}", y);
println!("w 指针地址: {:?}", w);
unsafe {
println!("y 指向的值: {}", *y);
println!("w 指向的值: {}", *w);
*w = 20; // 通过可变裸指针修改值
println!("修改后 z = {}", z);
}
}
3.2 裸指针与引用的区别¶
fn main() {
// 裸指针的特性:
// 1. 允许忽略借用规则,可以同时有可变和不可变指针
let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
// 安全代码中不允许同时有 &num 和 &mut num,但裸指针可以
// 2. 不保证指向有效内存
let address = 0x012345usize;
let _r = address as *const i32; // 可能指向无效内存
// 3. 允许为空
let null_ptr: *const i32 = std::ptr::null();
assert!(null_ptr.is_null());
// 4. 没有自动清理
unsafe {
println!("r1 = {}", *r1);
println!("r2 = {}", *r2);
}
}
3.3 指针运算¶
fn main() {
let arr = [10, 20, 30, 40, 50];
let ptr = arr.as_ptr();
unsafe {
// offset: 按元素偏移
for i in 0..5 {
let val = *ptr.offset(i as isize);
println!("arr[{}] = {}", i, val);
}
// add: 无符号偏移(常用)
let third = *ptr.add(2);
println!("第三个元素: {}", third);
// sub: 反向偏移
let end_ptr = ptr.add(4);
let second_last = *end_ptr.sub(1);
println!("倒数第二个: {}", second_last);
}
}
3.4 指针类型转换¶
use std::mem;
fn main() {
// 指针类型转换(cast)
let x: i32 = 42;
let ptr_i32: *const i32 = &x;
let ptr_u8: *const u8 = ptr_i32 as *const u8;
unsafe {
// 读取 i32 的第一个字节
println!("第一个字节: {}", *ptr_u8);
}
// 使用 cast 方法(更推荐)
let ptr_u8_v2: *const u8 = ptr_i32.cast::<u8>();
// 对齐指针
let data: [u8; 8] = [1, 2, 3, 4, 5, 6, 7, 8];
let ptr = data.as_ptr();
let aligned = unsafe { ptr.add(ptr.align_offset(mem::align_of::<u32>())) };
println!("对齐后的指针: {:?}", aligned);
}
3.5 NonNull 智能裸指针¶
use std::ptr::NonNull;
struct MyBox<T> {
ptr: NonNull<T>,
}
impl<T> MyBox<T> {
fn new(val: T) -> Self {
let boxed = Box::new(val);
let ptr = Box::into_raw(boxed);
// NonNull::new_unchecked 要求指针非空(Box保证了这一点)
MyBox {
ptr: unsafe { NonNull::new_unchecked(ptr) },
}
}
fn get(&self) -> &T {
unsafe { self.ptr.as_ref() }
}
fn get_mut(&mut self) -> &mut T {
unsafe { self.ptr.as_mut() }
}
}
impl<T> Drop for MyBox<T> {
fn drop(&mut self) {
unsafe {
// 从裸指针恢复 Box,让 Box 负责释放内存
let _ = Box::from_raw(self.ptr.as_ptr());
}
}
}
fn main() {
let mut my_box = MyBox::new(42);
println!("值: {}", my_box.get());
*my_box.get_mut() = 100;
println!("修改后: {}", my_box.get());
}
4. 手动内存管理¶
4.1 alloc / dealloc / Layout¶
use std::alloc::{self, Layout};
fn main() {
// 分配 10 个 i32 的内存
let layout = Layout::array::<i32>(10).unwrap();
unsafe {
// 分配内存
let ptr = alloc::alloc(layout) as *mut i32;
if ptr.is_null() {
alloc::handle_alloc_error(layout);
}
// 初始化内存
for i in 0..10 {
ptr.add(i).write(i as i32 * 10);
}
// 读取
for i in 0..10 {
println!("data[{}] = {}", i, *ptr.add(i));
}
// 释放内存
alloc::dealloc(ptr as *mut u8, layout);
}
}
4.2 alloc_zeroed 与 realloc¶
use std::alloc::{self, Layout};
fn main() {
unsafe {
// 分配零初始化内存
let layout = Layout::array::<u8>(1024).unwrap();
let ptr = alloc::alloc_zeroed(layout);
// 验证内存已清零
for i in 0..1024 {
assert_eq!(*ptr.add(i), 0);
}
// 重新分配(扩容)
let new_layout = Layout::array::<u8>(2048).unwrap();
let new_ptr = alloc::realloc(ptr, layout, new_layout.size());
if new_ptr.is_null() {
alloc::handle_alloc_error(new_layout);
}
// 新区域未初始化,需要手动清零
std::ptr::write_bytes(new_ptr.add(1024), 0, 1024);
alloc::dealloc(new_ptr, new_layout);
}
}
4.3 全局分配器¶
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};
/// 自定义分配器:追踪内存使用
struct TrackingAllocator {
inner: System,
allocated: AtomicUsize,
}
unsafe impl GlobalAlloc for TrackingAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let ptr = unsafe { self.inner.alloc(layout) };
if !ptr.is_null() {
self.allocated.fetch_add(layout.size(), Ordering::Relaxed);
}
ptr
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { self.inner.dealloc(ptr, layout) };
self.allocated.fetch_sub(layout.size(), Ordering::Relaxed);
}
}
#[global_allocator]
static ALLOCATOR: TrackingAllocator = TrackingAllocator {
inner: System,
allocated: AtomicUsize::new(0),
};
fn main() {
let before = ALLOCATOR.allocated.load(Ordering::Relaxed);
let v: Vec<i32> = vec![1, 2, 3, 4, 5];
let after = ALLOCATOR.allocated.load(Ordering::Relaxed);
println!("Vec 分配了 {} 字节", after - before);
drop(v);
let final_count = ALLOCATOR.allocated.load(Ordering::Relaxed);
println!("释放后剩余 {} 字节", final_count - before);
}
4.4 MaybeUninit¶
use std::mem::MaybeUninit;
fn main() {
// 安全地处理未初始化内存
let mut data: [MaybeUninit<i32>; 5] = [MaybeUninit::uninit(); 5];
// 逐个初始化
for (i, elem) in data.iter_mut().enumerate() {
elem.write(i as i32 * 10);
}
// 转换为已初始化数组
// 安全性保证:所有元素已被初始化
let data: [i32; 5] = unsafe {
// transmute 把 [MaybeUninit<i32>; 5] 转为 [i32; 5]
std::mem::transmute(data)
};
println!("{:?}", data); // [0, 10, 20, 30, 40]
}
// 更安全的方式:使用 assume_init
fn create_array() -> [String; 3] {
let mut arr: [MaybeUninit<String>; 3] = [
MaybeUninit::uninit(),
MaybeUninit::uninit(),
MaybeUninit::uninit(),
];
arr[0].write(String::from("hello"));
arr[1].write(String::from("world"));
arr[2].write(String::from("!"));
// 使用 map 逐个 assume_init
arr.map(|x| unsafe { x.assume_init() })
}
5. FFI 与 C 互操作¶
5.1 调用 C 函数¶
// 声明 C 函数签名
extern "C" {
fn strlen(s: *const std::os::raw::c_char) -> usize;
fn printf(format: *const std::os::raw::c_char, ...) -> i32;
}
fn main() {
// CString 确保以 null 结尾
let c_str = std::ffi::CString::new("Hello, FFI!").unwrap();
unsafe {
let len = strlen(c_str.as_ptr());
println!("C strlen 返回: {}", len);
}
}
5.2 C 类型映射¶
// Rust 与 C 类型对照表
// C 类型 -> Rust 类型
// int -> std::os::raw::c_int (i32)
// unsigned int -> std::os::raw::c_uint (u32)
// long -> std::os::raw::c_long
// char -> std::os::raw::c_char
// float -> f32
// double -> f64
// void* -> *mut std::os::raw::c_void
// const char* -> *const std::os::raw::c_char
// size_t -> usize
// bool -> bool (C99)
use std::os::raw::{c_char, c_int, c_void};
use std::ffi::{CStr, CString};
extern "C" {
fn atoi(s: *const c_char) -> c_int;
fn malloc(size: usize) -> *mut c_void;
fn free(ptr: *mut c_void);
}
fn safe_atoi(s: &str) -> Option<i32> {
let c_str = CString::new(s).ok()?;
Some(unsafe { atoi(c_str.as_ptr()) })
}
fn main() {
if let Some(num) = safe_atoi("42") {
println!("解析结果: {}", num);
}
}
5.3 CString 与 CStr¶
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
fn main() {
// Rust -> C: 使用 CString
let rust_string = String::from("Hello from Rust");
let c_string = CString::new(rust_string).unwrap();
let c_ptr: *const c_char = c_string.as_ptr();
// c_ptr 可以传递给 C 函数
// C -> Rust: 使用 CStr
unsafe {
let c_str: &CStr = CStr::from_ptr(c_ptr);
let rust_str: &str = c_str.to_str().unwrap();
println!("从 C 指针恢复: {}", rust_str);
}
// 注意:CString 拥有内存,CStr 只是借用
// CString 在 drop 时释放内存
// 如果 C 函数返回的指针需要由 C 释放,不要用 CString::from_raw
}
5.4 #[no_mangle] 与导出函数¶
// 供 C 代码调用的 Rust 函数
// #[no_mangle] 防止 Rust 编译器修改函数名
// extern "C" 使用 C ABI
/// 计算两个整数的和
///
/// # Safety
/// 此函数通过 FFI 调用,调用者需保证传入有效的 i32 值
#[no_mangle]
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 {
a + b
}
/// 创建一个字符串并返回指针
/// 调用者需要使用 rust_free_string 释放内存
#[no_mangle]
pub extern "C" fn rust_greeting(name: *const std::os::raw::c_char) -> *mut std::os::raw::c_char {
let c_str = unsafe {
assert!(!name.is_null());
std::ffi::CStr::from_ptr(name)
};
let name = c_str.to_str().unwrap_or("World");
let greeting = format!("Hello, {}!", name);
std::ffi::CString::new(greeting).unwrap().into_raw()
}
/// 释放 Rust 分配的字符串
///
/// # Safety
/// ptr 必须是由 rust_greeting 返回的有效指针
#[no_mangle]
pub unsafe extern "C" fn rust_free_string(ptr: *mut std::os::raw::c_char) {
if !ptr.is_null() {
unsafe {
let _ = std::ffi::CString::from_raw(ptr);
// CString 的 drop 会释放内存
}
}
}
5.5 使用 bindgen 自动生成绑定¶
// build.rs
fn main() {
// 生成 C 头文件的 Rust 绑定
let bindings = bindgen::Builder::default()
.header("wrapper.h") // C 头文件
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.expect("无法生成绑定");
// 输出到 $OUT_DIR/bindings.rs
let out_path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("无法写入绑定文件");
}
// wrapper.h - C 头文件示例
#ifndef MY_LIB_H
#define MY_LIB_H
typedef struct {
int x;
int y;
} Point;
Point point_new(int x, int y);
double point_distance(const Point* a, const Point* b);
void point_free(Point* p);
#endif
// src/lib.rs - 使用生成的绑定
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
// 引入 bindgen 生成的绑定
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
// 创建安全的 Rust 封装
pub struct SafePoint {
inner: Point,
}
impl SafePoint {
pub fn new(x: i32, y: i32) -> Self {
let inner = unsafe { point_new(x, y) };
SafePoint { inner }
}
pub fn distance(&self, other: &SafePoint) -> f64 {
unsafe { point_distance(&self.inner, &other.inner) }
}
}
5.6 使用 cbindgen 生成 C 头文件¶
// build.rs
fn main() {
let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
cbindgen::Builder::new()
.with_crate(crate_dir)
.with_language(cbindgen::Language::C)
.generate()
.expect("无法生成 C 头文件")
.write_to_file("include/mylib.h");
}
// src/lib.rs - 标注 #[repr(C)] 确保内存布局与 C 兼容
/// 二维向量
#[repr(C)]
pub struct Vec2 {
pub x: f64,
pub y: f64,
}
/// 创建新向量
#[no_mangle]
pub extern "C" fn vec2_new(x: f64, y: f64) -> Vec2 {
Vec2 { x, y }
}
/// 计算向量长度
#[no_mangle]
pub extern "C" fn vec2_length(v: &Vec2) -> f64 {
(v.x * v.x + v.y * v.y).sqrt()
}
/// 向量相加
#[no_mangle]
pub extern "C" fn vec2_add(a: &Vec2, b: &Vec2) -> Vec2 {
Vec2 {
x: a.x + b.x,
y: a.y + b.y,
}
}
6. 安全抽象:将 unsafe 封装为安全 API¶
6.1 安全抽象的设计原则¶
/// 安全抽象示例:自定义切片分割
///
/// 标准库的 split_at_mut 就是一个安全抽象的典范
fn split_at_mut<T>(slice: &mut [T], mid: usize) -> (&mut [T], &mut [T]) {
let len = slice.len();
let ptr = slice.as_mut_ptr();
// 前置条件检查(在 safe 代码中完成)
assert!(mid <= len, "mid ({}) > len ({})", mid, len);
// unsafe 块:仅包含必需的 unsafe 操作
unsafe {
(
std::slice::from_raw_parts_mut(ptr, mid),
std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
fn main() {
let mut data = vec![1, 2, 3, 4, 5];
let (left, right) = split_at_mut(&mut data, 3);
left[0] = 10;
right[0] = 40;
println!("left: {:?}, right: {:?}", left, right);
}
6.2 类型状态模式保证安全¶
use std::marker::PhantomData;
// 类型状态标记
struct Locked;
struct Unlocked;
/// 保险箱:使用类型状态确保只有解锁后才能读取内容
struct Safe<State> {
content: String,
_state: PhantomData<State>,
}
impl Safe<Locked> {
fn new(content: String) -> Self {
Safe {
content,
_state: PhantomData,
}
}
fn unlock(self, password: &str) -> Result<Safe<Unlocked>, Safe<Locked>> {
if password == "secret" {
Ok(Safe {
content: self.content,
_state: PhantomData,
})
} else {
Err(self)
}
}
}
impl Safe<Unlocked> {
fn read(&self) -> &str {
&self.content
}
fn lock(self) -> Safe<Locked> {
Safe {
content: self.content,
_state: PhantomData,
}
}
}
fn main() {
let safe = Safe::new("机密文件".to_string());
// safe.read(); // ❌ 编译错误:Safe<Locked> 没有 read 方法
match safe.unlock("secret") {
Ok(unlocked) => {
println!("内容: {}", unlocked.read()); // ✅ 可以读取
let _locked = unlocked.lock();
}
Err(_) => println!("密码错误"),
}
}
6.3 封装 unsafe 集合¶
use std::alloc::{self, Layout};
use std::ptr;
/// 简易的固定大小栈
///
/// # Safety Invariants (安全不变量)
/// - `data` 指向一块已分配的、对齐的、容量为 `capacity` 个 `T` 的内存
/// - `len <= capacity`
/// - `data[0..len]` 中的所有元素均已初始化且有效
/// - `data[len..capacity]` 中的内存未初始化
pub struct FixedStack<T> {
data: *mut T,
len: usize,
capacity: usize,
}
impl<T> FixedStack<T> {
/// 创建指定容量的栈
pub fn new(capacity: usize) -> Self {
assert!(capacity > 0, "容量必须大于0");
let layout = Layout::array::<T>(capacity).unwrap();
let data = unsafe { alloc::alloc(layout) as *mut T };
if data.is_null() {
alloc::handle_alloc_error(layout);
}
FixedStack {
data,
len: 0,
capacity,
}
}
/// 压栈(安全 API)
pub fn push(&mut self, val: T) -> Result<(), T> {
if self.len >= self.capacity {
return Err(val); // 栈满,返回值
}
unsafe {
self.data.add(self.len).write(val);
}
self.len += 1;
Ok(())
}
/// 弹栈(安全 API)
pub fn pop(&mut self) -> Option<T> {
if self.len == 0 {
return None;
}
self.len -= 1;
Some(unsafe { self.data.add(self.len).read() })
}
/// 查看栈顶
pub fn peek(&self) -> Option<&T> {
if self.len == 0 {
return None;
}
Some(unsafe { &*self.data.add(self.len - 1) })
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
}
impl<T> Drop for FixedStack<T> {
fn drop(&mut self) {
// 逐个 drop 元素
while self.pop().is_some() {}
// 释放内存
let layout = Layout::array::<T>(self.capacity).unwrap();
unsafe {
alloc::dealloc(self.data as *mut u8, layout);
}
}
}
// Safety: FixedStack 独占 data 指向的内存,与 Box<[T]> 的所有权语义一致。
// 如果 T: Send,则 FixedStack<T> 可以安全地跨线程传递。
unsafe impl<T: Send> Send for FixedStack<T> {}
// Safety: FixedStack 只通过 &self 提供只读访问(peek),
// 如果 T: Sync,则多线程只读访问是安全的。
unsafe impl<T: Sync> Sync for FixedStack<T> {}
fn main() {
let mut stack = FixedStack::new(3);
stack.push(1).unwrap();
stack.push(2).unwrap();
stack.push(3).unwrap();
assert!(stack.push(4).is_err()); // 栈满
while let Some(val) = stack.pop() {
println!("popped: {}", val);
}
}
7. Pin 与自引用结构¶
7.1 自引用结构的问题¶
/// 自引用结构:一个字段引用另一个字段
/// 这在 Rust 中非常棘手,因为移动会使引用失效
// ❌ 这个结构体无法安全工作
struct SelfRef {
data: String,
// ptr 指向 data 的内容,但如果 SelfRef 被移动,ptr 就会悬空
ptr: *const String,
}
impl SelfRef {
fn new(data: String) -> Self {
let mut s = SelfRef {
data,
ptr: std::ptr::null(),
};
s.ptr = &s.data; // 指向自身
s
// ⚠️ 返回时 s 被移动,ptr 变成悬空指针!
}
}
7.2 Pin 的作用¶
use std::pin::Pin;
use std::marker::PhantomPinned;
/// 使用 Pin 的自引用结构
struct PinnedSelfRef {
data: String,
slice: *const str,
_pin: PhantomPinned, // 标记为 !Unpin
}
impl PinnedSelfRef {
fn new(data: String) -> Pin<Box<Self>> {
let res = PinnedSelfRef {
data,
slice: std::ptr::null(),
_pin: PhantomPinned,
};
let mut boxed = Box::pin(res);
// 初始化自引用
let slice: *const str = &boxed.data;
unsafe {
let mut_ref = Pin::as_mut(&mut boxed);
Pin::get_unchecked_mut(mut_ref).slice = slice;
}
boxed
}
fn data(&self) -> &str {
&self.data
}
fn slice(&self) -> &str {
unsafe { &*self.slice }
}
}
fn main() {
let pinned = PinnedSelfRef::new("Hello, Pin!".to_string());
println!("data: {}", pinned.data());
println!("slice: {}", pinned.slice());
// pinned 不能被移动,自引用始终有效
}
7.3 Pin 与 Future¶
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
/// 自定义 Future 示例
struct Delay {
duration: std::time::Duration,
started: bool,
}
impl Delay {
fn new(duration: std::time::Duration) -> Self {
Delay {
duration,
started: false,
}
}
}
impl Future for Delay {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
// Pin<&mut Self> 保证 self 不会被移动
let this = self.get_mut();
if !this.started {
this.started = true;
// 实际实现中会注册定时器
let waker = cx.waker().clone();
let duration = this.duration;
std::thread::spawn(move || {
std::thread::sleep(duration);
waker.wake();
});
Poll::Pending
} else {
Poll::Ready(())
}
}
}
7.4 pin! 宏的使用¶
use std::pin::pin;
async fn example() {
let future = async { 42 };
// pin! 宏将值固定在栈上
let pinned = pin!(future);
// pinned 的类型是 Pin<&mut impl Future<Output = i32>>
// 可以安全地 poll
println!("Future 已被 pin 到栈上");
}
// 在容器中使用 Pin
fn collect_futures() -> Vec<Pin<Box<dyn Future<Output = i32>>>> {
vec![
Box::pin(async { 1 }),
Box::pin(async { 2 }),
Box::pin(async { 3 }),
]
}
8. 常见 unsafe 陷阱和 UB(未定义行为)¶
8.1 什么是未定义行为(UB)¶
/// Rust 中的未定义行为列表:
/// 1. 解引用悬空指针或空指针
/// 2. 读取未初始化内存
/// 3. 违反别名规则(同时存在可变和不可变引用)
/// 4. 产生无效的基本类型值(如 bool 不是 0 或 1)
/// 5. 数据竞争
/// 6. 解引用未对齐的指针
/// 7. 调用 ABI 不兼容的函数
/// 8. 违反类型的不变量(如 &T 必须对齐且非空)
// 示例:各种 UB
fn ub_examples() {
// ❌ UB 1: 悬空指针
let ptr: *const i32 = {
let x = 42;
&x as *const i32
}; // x 已被销毁,ptr 悬空
// unsafe { println!("{}", *ptr); } // UB!
// ❌ UB 2: 读取未初始化内存
// let x: i32;
// println!("{}", x); // 编译器会阻止,但 unsafe 中可能发生
// ❌ UB 3: 违反别名规则
let mut data = vec![1, 2, 3];
let ptr = data.as_mut_ptr();
// 同时通过指针和引用访问
// unsafe { *ptr = 10; }
// println!("{}", data[0]); // 可能 UB(取决于具体使用方式)
// ❌ UB 4: 无效的 bool 值
// let b: bool = unsafe { std::mem::transmute(2u8) }; // UB! bool 只能是 0 或 1
}
8.2 use-after-free¶
fn use_after_free_example() {
let ptr: *const String;
{
let s = String::from("hello");
ptr = &s as *const String;
// s 在这里被 drop
}
// ❌ ptr 现在是悬空指针
// unsafe { println!("{}", *ptr); } // use-after-free, UB!
}
// ✅ 安全替代方案
fn safe_alternative() {
let s = String::from("hello");
let r: &String = &s; // 借用检查器保证 r 不会比 s 活得久
println!("{}", r);
}
8.3 数据竞争¶
use std::thread;
// ❌ 数据竞争 UB 示例(概念性代码,不要运行)
fn data_race_example() {
let mut data = 0u32;
let ptr = &mut data as *mut u32;
// 多线程通过裸指针访问同一数据 = 数据竞争
// let ptr_clone = ptr as usize;
// thread::spawn(move || {
// let p = ptr_clone as *mut u32;
// unsafe { *p += 1; } // UB: 数据竞争
// });
// unsafe { *ptr += 1; } // UB: 数据竞争
}
// ✅ 安全替代方案:使用原子操作或 Mutex
use std::sync::{Arc, Mutex};
fn safe_concurrent() {
let data = Arc::new(Mutex::new(0u32));
let handles: Vec<_> = (0..10)
.map(|_| {
let data = Arc::clone(&data);
thread::spawn(move || {
let mut guard = data.lock().unwrap();
*guard += 1;
})
})
.collect();
for h in handles {
h.join().unwrap();
}
println!("结果: {}", *data.lock().unwrap());
}
8.4 transmute 的危险¶
use std::mem;
fn transmute_dangers() {
// ❌ 危险:不同大小的类型
// let x: u64 = unsafe { mem::transmute(0u32) }; // 编译错误,大小不同
// ❌ 危险:创建无效的枚举值
#[repr(u8)]
enum Color {
Red = 0,
Green = 1,
Blue = 2,
}
// let c: Color = unsafe { mem::transmute(42u8) }; // UB! 42 不是有效的 Color
// ✅ 使用 transmute_copy 和验证
fn from_u8(val: u8) -> Option<Color> {
match val {
0 => Some(Color::Red),
1 => Some(Color::Green),
2 => Some(Color::Blue),
_ => None,
}
}
// ✅ 安全的类型转换
let bytes: [u8; 4] = [0x78, 0x56, 0x34, 0x12];
let num = u32::from_le_bytes(bytes); // 使用专用函数代替 transmute
println!("0x{:08X}", num);
}
8.5 未对齐访问¶
fn unaligned_access() {
let data: [u8; 8] = [1, 2, 3, 4, 5, 6, 7, 8];
// ❌ 可能未对齐
// let ptr = &data[1] as *const u8 as *const u32;
// unsafe { println!("{}", *ptr); } // UB: 可能未对齐
// ✅ 使用 read_unaligned
let ptr = &data[1] as *const u8 as *const u32;
let val = unsafe { ptr.read_unaligned() };
println!("未对齐读取: {}", val);
// ✅ 使用 from_ne_bytes
let bytes = &data[1..5];
let val = u32::from_ne_bytes(bytes.try_into().unwrap());
println!("安全读取: {}", val);
}
9. Miri 工具检测 UB¶
9.1 安装和使用 Miri¶
# 安装 Miri
rustup +nightly component add miri
# 运行 Miri 检测 UB
cargo +nightly miri test
# 运行特定测试
cargo +nightly miri test test_name
# 运行程序
cargo +nightly miri run
# 设置 Miri 标志
MIRIFLAGS="-Zmiri-disable-isolation" cargo +nightly miri test
9.2 Miri 能检测的 UB¶
#[cfg(test)]
mod tests {
// Miri 可以检测的问题:
// ✓ use-after-free
// ✓ 越界访问
// ✓ 使用未初始化数据
// ✓ 违反别名规则(Stacked Borrows / Tree Borrows)
// ✓ 内存泄漏
// ✓ 数据竞争
// ✓ 未对齐的指针访问
// ✓ 无效的枚举/bool值
#[test]
fn test_valid_unsafe() {
// 有效的 unsafe 代码,Miri 应通过
let mut data = vec![1, 2, 3, 4, 5];
let ptr = data.as_mut_ptr();
unsafe {
for i in 0..5 {
*ptr.add(i) *= 2;
}
}
assert_eq!(data, vec![2, 4, 6, 8, 10]);
}
#[test]
fn test_out_of_bounds() {
// Miri 会检测到越界访问
let data = vec![1, 2, 3];
let ptr = data.as_ptr();
unsafe {
let _valid = *ptr.add(2); // OK
// let _invalid = *ptr.add(3); // Miri 会报错!
}
}
#[test]
fn test_aliasing() {
// 测试别名规则
let mut data = 42i32;
let ptr1 = &mut data as *mut i32;
unsafe {
*ptr1 = 100;
assert_eq!(*ptr1, 100);
}
}
}
9.3 Miri 配置¶
# .cargo/config.toml
[target.'cfg(miri)']
runner = "cargo miri"
# 环境变量配置
# MIRIFLAGS="-Zmiri-tag-gc=1000" # GC频率
# MIRIFLAGS="-Zmiri-tree-borrows" # 使用 Tree Borrows 模型
# MIRIFLAGS="-Zmiri-disable-stacked-borrows" # 禁用 Stacked Borrows
// 在代码中检测是否在 Miri 下运行
#[cfg(miri)]
fn running_under_miri() {
println!("当前在 Miri 下运行");
// Miri 下跳过某些测试(如网络/文件IO)
}
#[cfg(not(miri))]
fn running_under_miri() {
println!("正常运行");
}
// 条件编译测试
#[cfg(test)]
mod tests {
#[test]
#[cfg_attr(miri, ignore)] // Miri 下忽略(例如需要网络的测试)
fn test_network() {
// 网络测试...
}
#[test]
fn test_memory_safety() {
// 这个测试在 Miri 下运行
let v = vec![1, 2, 3];
assert_eq!(v[1], 2);
}
}
10. 面试题¶
Q1: 为什么 Rust 需要 unsafe?它提供了哪些能力?¶
A1:
Rust 的安全保证依赖编译期静态分析,但有些操作的安全性无法在编译期验证:
// unsafe 提供五种"超能力":
// 1. 解引用裸指针
let x = 42;
let ptr = &x as *const i32;
let val = unsafe { *ptr };
// 2. 调用 unsafe 函数
unsafe fn dangerous_fn() {}
unsafe { dangerous_fn(); }
// 3. 访问或修改可变静态变量
static mut COUNTER: i32 = 0;
unsafe { COUNTER += 1; }
// 4. 实现 unsafe trait
unsafe trait Scary {}
unsafe impl Scary for i32 {}
// 5. 访问 union 的字段
union MyUnion { i: i32, f: f32 }
let u = MyUnion { i: 1 };
let val = unsafe { u.i };
需要 unsafe 的根本原因: - FFI 调用(与 C/C++ 库交互) - 实现底层数据结构(如 Vec、LinkedList) - 直接硬件访问(嵌入式、OS开发) - 性能关键路径(跳过边界检查)
核心理念: unsafe 不是"不安全的代码",而是"编译器无法验证安全性、由程序员负责保证"的代码。
Q2: 如何安全地封装 unsafe 代码?¶
A2:
安全封装的核心原则:对外暴露安全 API,内部用 unsafe 实现。
/// 安全封装示例:从两个不可变切片创建可变切片
/// 前提:两个范围不重叠
pub fn split_at_mut<T>(slice: &mut [T], mid: usize) -> (&mut [T], &mut [T]) {
let len = slice.len();
let ptr = slice.as_mut_ptr();
// 1. 在安全代码中验证前置条件
assert!(mid <= len);
// 2. unsafe 块最小化
unsafe {
(
std::slice::from_raw_parts_mut(ptr, mid),
std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
// 3. 返回安全类型(&mut [T]),调用者无需 unsafe
}
封装检查清单: 1. 前置条件在 safe 代码中验证(assert! / 返回 Result) 2. unsafe 块尽可能小 3. 文档化 safety invariants(/// # Safety 注释) 4. 返回安全类型 5. 编写测试 + Miri 检测
Q3: 什么是未定义行为(UB)?列举常见的 UB¶
A3:
// UB = 编译器不保证程序行为的代码,可能导致任何结果
// 常见 UB 列表:
// 1. 解引用空指针或悬空指针
let ptr: *const i32 = std::ptr::null();
// unsafe { *ptr }; // UB
// 2. 读取未初始化内存
// let x: i32 = unsafe { std::mem::MaybeUninit::uninit().assume_init() }; // UB
// 3. 创建无效值
// let b: bool = unsafe { std::mem::transmute(2u8) }; // UB (bool 只能是 0 或 1)
// 4. 违反别名规则
// 同时存在 &T 和 &mut T 指向同一数据
// 5. 数据竞争
// 多线程无同步地同时读写同一内存
// 6. 未对齐的引用
// let p: &u32 = unsafe { &*(addr as *const u32) }; // 如果 addr 未对齐则 UB
// 7. 违反函数前置条件
// unsafe { "hello".get_unchecked(10..) }; // 越界 UB
检测工具: Miri(cargo +nightly miri test)可以检测大部分 UB。
Q4: 裸指针(*const T / *mut T)与引用(&T / &mut T)有什么区别?¶
A4:
fn main() {
let mut x = 42;
// 引用的约束:
let r: &i32 = &x; // 1. 必须有效(非空、非悬空)
// let r2: &mut i32 = &mut x; // 2. 借用规则(不能同时存在&和&mut)
// 3. 自动 deref
// 4. 有生命周期
// 裸指针的自由:
let p1: *const i32 = &x; // 1. 可以为空
let p2: *mut i32 = &mut x; // 2. 可以同时有 *const 和 *mut
// 3. 不安全 deref(需要 unsafe)
// 4. 没有生命周期
// 5. 可以进行指针运算
// 创建裸指针是安全的,使用裸指针才需要 unsafe
println!("指针地址: {:?}", p1);
unsafe {
println!("值: {}", *p1);
}
}
| 特性 | 引用 &T / &mut T | 裸指针 *const T / *mut T |
|---|---|---|
| 空值 | 不允许 | 允许 |
| 对齐 | 保证对齐 | 不保证 |
| 有效性 | 保证有效 | 不保证 |
| 别名规则 | 编译器检查 | 程序员负责 |
| 生命周期 | 有 | 无 |
| 解引用 | 安全 | unsafe |
Q5: FFI 中如何正确处理字符串?¶
A5:
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
// Rust -> C: 使用 CString
fn send_to_c(s: &str) {
// CString::new 会追加 \0,并检查字符串中没有内部 \0
let c_string = CString::new(s).expect("字符串包含内部 null 字节");
let ptr: *const c_char = c_string.as_ptr();
// ptr 在 c_string 存活期间有效
unsafe { some_c_function(ptr); }
}
// C -> Rust: 使用 CStr
unsafe fn receive_from_c(ptr: *const c_char) -> String {
// CStr::from_ptr 会找到 \0 终止符
let c_str = unsafe { CStr::from_ptr(ptr) };
// to_string_lossy 处理非 UTF-8 数据
c_str.to_string_lossy().into_owned()
}
extern "C" { fn some_c_function(s: *const c_char); }
要点: - CString(拥有所有权)vs CStr(借用) - Rust 字符串是 UTF-8、无 null 终止;C 字符串是 null 终止、可能非 UTF-8 - 注意 CString 的生命周期,不要返回其内部指针后立即 drop
Q6: Pin 解决了什么问题?async/await 为什么需要 Pin?¶
A6:
use std::pin::Pin;
use std::marker::PhantomPinned;
// 问题:自引用结构移动后,内部指针悬空
struct SelfRef {
data: String,
ptr: *const String, // 指向 self.data
}
// 如果 SelfRef 被移动(如 Vec 扩容),ptr 变成悬空指针
// async/await 编译后生成的 Future 是自引用结构:
async fn example() {
let data = String::from("hello");
some_async_op(&data).await; // await 点跨越了 data 的借用
println!("{}", data); // data 在 await 后仍被使用
}
// 编译器生成的状态机结构体中,同时保存了 data 和对 data 的引用
// → 这就是自引用结构
// Pin 的解决方案:
// Pin<P> 保证 P 指向的值不会被移动(除非实现 Unpin)
// Future::poll 签名使用 Pin<&mut Self>
async fn some_async_op(_: &str) {}
// 核心规则:
// - Unpin 类型可以自由移动(大部分标准类型都是 Unpin)
// - !Unpin 类型被 Pin 后不能移动(自引用结构标记为 !Unpin)
// - PhantomPinned 是标记 !Unpin 的零大小标记类型
✅ 学习检查清单¶
基础概念¶
- 理解 unsafe 的五种超能力(裸指针、unsafe函数、可变静态变量、unsafe trait、union字段访问)
- 知道何时应该和不应该使用 unsafe
- 理解 unsafe 块的作用域和边界
裸指针¶
- 掌握
*const T和*mut T的创建和使用 - 理解裸指针与引用的区别
- 会进行指针运算(offset/add/sub)
- 了解
NonNull<T>的用途
内存管理¶
- 掌握
alloc/dealloc/Layout - 理解
MaybeUninit的作用 - 了解全局分配器
GlobalAlloc
FFI¶
- 掌握
extern "C"声明和调用 C 函数 - 熟练使用
CString/CStr处理字符串 - 了解
#[no_mangle]和#[repr(C)] - 知道 bindgen / cbindgen 工具的用途
安全抽象¶
- 能将 unsafe 代码封装为安全 API
- 理解 safety invariants 的概念
- 会文档化 unsafe 代码的安全性要求
Pin 与自引用¶
- 理解自引用结构的问题
- 掌握
Pin的作用和使用方式 - 理解
Unpintrait - 知道 async/await 为何需要 Pin
UB 检测¶
- 能列举常见的 UB 类型
- 会使用 Miri 检测 UB
- 了解 Miri 的配置选项
📌 核心要点: unsafe 不是逃生门,而是一种契约。编写 unsafe 代码意味着你在向编译器保证"我已经验证了安全性"。最小化 unsafe 范围、充分文档化、使用 Miri 测试,是写好 unsafe Rust 的三大原则。