在之前的十二篇文章中,我们一直在安全 Rust(Safe Rust) 的世界里------所有权、借用检查、生命周期,编译器替我们挡住了所有内存安全问题。但系统编程的某些场景,安全 Rust 无法表达:调用 C 库、操作裸指针、实现底层数据结构、直接与操作系统交互。这时,就需要 unsafe Rust。unsafe 不是"关闭安全检查",而是开启五项编译器无法验证的能力,并将安全责任转移给开发者。本文从 unsafe 的五种能力讲起,深入讲解裸指针、FFI(外部函数接口)、bindgen/cbindgen 工具,并通过一个完整的 C 库封装实战,帮你掌握安全使用 unsafe 的方法论。
一、unsafe 的五种能力
unsafe 关键字解锁了五种编译器无法验证的操作:

💡 关键理解:unsafe 不会关闭借用检查或类型检查。它只允许你做上述五种操作。编译器仍然检查类型安全和借用规则。
二、裸指针(Raw Pointer)
裸指针 *const T 和 *mut T 与 C 的指针类似,但不受 Rust 借用规则约束。
2.1 创建和使用裸指针
rust
fn main() {
let mut num = 5;
// 创建裸指针(安全操作)
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
// 解引用裸指针(unsafe 操作)
unsafe {
println!("r1 = {}", *r1);
println!("r2 = {}", *r2);
*r2 = 10; // 通过可变裸指针修改
println!("num = {}", num); // 10
}
}
裸指针与引用的区别:

2.2 裸指针的典型用途
场景一:与 C 代码交互
rust
extern "C" {
fn malloc(size: usize) -> *mut u8;
fn free(ptr: *mut u8);
}
fn main() {
unsafe {
let ptr = malloc(1024);
if !ptr.is_null() {
// 使用内存...
free(ptr);
}
}
}
场景二:实现底层数据结构
rust
struct LinkedList<T> {
head: *mut Node<T>,
len: usize,
}
struct Node<T> {
value: T,
next: *mut Node<T>,
}
impl<T> LinkedList<T> {
fn new() -> Self {
LinkedList {
head: std::ptr::null_mut(),
len: 0,
}
}
fn push(&mut self, value: T) {
let new_node = Box::into_raw(Box::new(Node {
value,
next: self.head,
}));
self.head = new_node;
self.len += 1;
}
}
三、unsafe 函数
unsafe fn 表示调用者必须满足某些前提条件:
rust
/// # Safety
///
/// `ptr` 必须是指向有效 `i32` 的非空指针。
unsafe fn read_value(ptr: *const i32) -> i32 {
*ptr
}
fn main() {
let x = 42;
let value = unsafe { read_value(&x) };
println!("value = {}", value);
}
最佳实践:为 unsafe fn 添加 # Safety 文档注释,说明调用者必须满足的条件。
四、安全抽象:封装 unsafe 代码
unsafe 代码应该被封装在安全抽象中。这是 Rust 生态的核心原则:用最小范围的 unsafe 实现功能,对外暴露安全 API。
4.1 封装示例:安全的 split_at_mut
标准库的 split_at_mut 可以在一个可变切片中安全地创建两个不重叠的可变切片:
rust
use std::slice;
fn split_at_mut_safe<T>(slice: &mut [T], mid: usize) -> (&mut [T], &mut [T]) {
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, 6];
let (left, right) = split_at_mut_safe(&mut v, 3);
left[0] = 10;
right[0] = 40;
println!("{:?}", v); // [10, 2, 3, 40, 5, 6]
}
封装要点:
外部 API 是安全的(无 unsafe)。
assert! 检查前置条件。
unsafe 块只包含必要的操作。
五、FFI:外部函数接口
FFI(Foreign Function Interface) 让 Rust 可以调用其他语言(主要是 C)的函数,也可以被其他语言调用。
5.1 调用 C 函数
rust
use std::os::raw::{c_char, c_int};
// 声明外部 C 函数
extern "C" {
fn strlen(s: *const c_char) -> usize;
fn puts(s: *const c_char) -> c_int;
}
fn main() {
let s = std::ffi::CString::new("Hello from Rust!").unwrap();
unsafe {
puts(s.as_ptr());
let len = strlen(s.as_ptr());
println!("字符串长度: {}", len);
}
}
C 类型与 Rust 类型的对应:
C 类型 Rust 类型

5.2 导出 Rust 函数给 C
rust
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
a + b
}
#[no_mangle]
pub extern "C" fn greet(name: *const std::os::raw::c_char) {
let name = unsafe { std::ffi::CStr::from_ptr(name) };
println!("Hello, {}!", name.to_str().unwrap());
}
关键属性:
#no_mangle:阻止 Rust 修改函数名。
extern "C":使用 C 调用约定。
生成 C 头文件:
toml
lib
crate-type = "cdylib" # 生成动态库
编译后生成 .so(Linux)或 .dll(Windows),C 程序可以链接使用。
5.3 处理字符串
Rust 字符串 → C 字符串:
rust
use std::ffi::CString;
let rust_str = "hello";
let c_str = CString::new(rust_str).unwrap();
let ptr = c_str.as_ptr(); // *const c_char
C 字符串 → Rust 字符串:
rust
use std::ffi::CStr;
let c_str: *const c_char = ...;
let rust_str = unsafe { CStr::from_ptr(c_str) }
.to_str()
.unwrap();
⚠️ 注意:CString 拥有所有权,CStr 是借用。CString::new 会检查字符串中是否包含空字节。
六、bindgen 与 cbindgen:自动生成绑定
手动编写 FFI 绑定容易出错。社区提供了两个工具:
6.1 bindgen:从 C 头文件生成 Rust 绑定
bash
cargo install bindgen-cli
rust
// wrapper.h
#include "mylib.h"
bash
bindgen wrapper.h -o src/bindings.rs
生成的 bindings.rs 包含 C 函数和类型的 Rust 声明。
在 build.rs 中自动生成:
rust
// build.rs
fn main() {
let bindings = bindgen::Builder::default()
.header("wrapper.h")
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.expect("生成绑定失败");
let out_path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("写入绑定失败");
}
6.2 cbindgen:从 Rust 生成 C 头文件
bash
cargo install cbindgen
bash
cbindgen --config cbindgen.toml --crate my_crate --output my_crate.h
七、实战:封装一个 C 库
假设我们有一个 C 库实现了简单的计算器:
c
// calculator.h
typedef struct {
double result;
int error_code;
} CalcResult;
CalcResult calc_add(double a, double b);
CalcResult calc_div(double a, double b);
Step 1:创建 Rust 项目
toml
package
name = "calculator"
version = "0.1.0"
edition = "2021"
dependencies
libc = "0.2"
build-dependencies
cc = "1"
Step 2:编写安全封装
rust
use std::os::raw::c_double;
#[repr(C)]
struct CalcResult {
result: c_double,
error_code: i32,
}
extern "C" {
fn calc_add(a: c_double, b: c_double) -> CalcResult;
fn calc_div(a: c_double, b: c_double) -> CalcResult;
}
// ===== 安全抽象 =====
#[derive(Debug)]
pub enum CalcError {
DivisionByZero,
Unknown(i32),
}
pub fn add(a: f64, b: f64) -> Result<f64, CalcError> {
let result = unsafe { calc_add(a, b) };
check_result(result)
}
pub fn div(a: f64, b: f64) -> Result<f64, CalcError> {
let result = unsafe { calc_div(a, b) };
check_result(result)
}
fn check_result(result: CalcResult) -> Result<f64, CalcError> {
match result.error_code {
0 => Ok(result.result),
1 => Err(CalcError::DivisionByZero),
code => Err(CalcError::Unknown(code)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2.0, 3.0).unwrap(), 5.0);
}
#[test]
fn test_div_by_zero() {
assert!(matches!(div(1.0, 0.0), Err(CalcError::DivisionByZero)));
}
}
Step 3:编译 C 代码(build.rs)
rust
// build.rs
fn main() {
cc::Build::new()
.file("src/calculator.c")
.compile("calculator");
}
核心设计原则:
unsafe 只出现在最小的范围内。
对外暴露的是安全的 Result<f64, CalcError>。
内部错误码被转换为 Rust 的 enum。
提供单元测试确保封装正确。
八、unsafe 的最佳实践
最小化 unsafe 范围:unsafe 块应尽可能小,只包含必须 unsafe 的操作。
封装为安全抽象:对外暴露安全 API,将 unsafe 隐藏在内部。
文档化 Safety 条件:为 unsafe fn 添加 # Safety 注释。
使用 debug_assert! 验证前置条件:在 debug 模式下检查不变量。
用 Miri 检测未定义行为:cargo +nightly miri test 可以检测 unsafe 代码中的 UB。
优先使用标准库的安全抽象:如 std::slice::from_raw_parts_mut 代替手写指针运算。
考虑使用 bytemuck、zerocopy 等库:它们提供了经过验证的安全类型转换。
8.1 使用 Miri 检测 UB
bash
rustup +nightly component add miri
cargo +nightly miri test
Miri 是一个 MIR 解释器,可以检测未定义行为(如越界访问、使用未初始化内存、数据竞争)。
九、小结
unsafe 的五种能力:解引用裸指针、调用 unsafe 函数、访问可变静态变量、实现 unsafe trait、访问 union 字段。
unsafe 不关闭借用检查和类型检查------它只解锁编译器无法验证的操作。
裸指针:*const T / *mut T,可为 null,不受借用规则约束。
FFI:extern "C" 声明外部函数,#no_mangle 导出 Rust 函数。
bindgen / cbindgen:自动生成 C 与 Rust 的绑定代码。
安全抽象:unsafe 代码应封装在安全 API 内部,最小化 unsafe 范围。
Miri:检测 unsafe 代码中的未定义行为。