C 开发者第一次看到 unsafe,很容易把它理解成“Rust 里可以写 C 的地方”。
这个理解太粗。
unsafe 不是关闭 Rust 所有检查,也不是把整段代码变回 C。它只允许你做少数编译器无法静态证明安全的操作,例如解引用裸指针、调用 FFI 函数、访问可变全局变量。
真正重要的不是“能不能写 unsafe”,而是:把这些风险限制在多小的边界里。
C 默认就是 unsafe 边界
C 里很多操作都靠程序员保证前提:
int read_byte(const uint8_t *ptr)
{
return ptr[0];
}
调用方必须保证:
ptr不是NULLptr指向至少 1 字节有效内存- 这段内存在访问期间没有被释放
- 当前访问不会和其他写入产生数据竞争
C 类型无法完整表达这些前提。
Rust 安全代码默认不允许直接做这类事。要解引用裸指针,必须进入 unsafe:
unsafe fn read_byte(ptr: *const u8) -> u8 {
unsafe { *ptr }
}
这不是因为解引用本身一定错,而是因为编译器无法证明 ptr 一定有效。
unsafe 允许少数额外操作
unsafe 主要允许几类操作:
- 解引用裸指针
- 调用
unsafe fn - 访问或修改
static mut - 实现
unsafe trait - 访问 union 字段
普通 Rust 规则仍然存在。类型检查、所有权、借用、匹配、错误处理不会消失。
例如:
unsafe fn read_byte(ptr: *const u8) -> u8 {
if ptr.is_null() {
return 0;
}
unsafe { *ptr }
}
if、return、类型检查都还是正常 Rust。只有 *ptr 这一步需要 unsafe,因为它依赖额外前提:这个指针虽然非空,但还必须可读、对齐、指向有效对象。
unsafe fn 表示调用方也要承担前提
如果一个函数本身要求调用方保证某些安全条件,可以把函数标成 unsafe fn:
unsafe fn slice_from_raw<'a>(ptr: *const u8, len: usize) -> &'a [u8] {
unsafe { std::slice::from_raw_parts(ptr, len) }
}
调用这个函数的人必须保证:
ptr指向len字节有效内存- 这段内存至少在返回的 slice 使用期间有效
- 内存没有被同时可变访问
len和实际可访问范围匹配
这些前提必须写在文档里,通常用 # Safety 说明。
unsafe fn 的意思不是“函数内部必然危险”,而是“安全调用这个函数需要调用方满足额外前提”。
安全封装要把前提关在里面
更好的做法通常是:让 unsafe 很小,把它封装在安全 API 后面。
例如:
fn first_byte(data: &[u8]) -> Option<u8> {
data.first().copied()
}
这个函数完全不需要 unsafe,因为 slice 已经保证了指针和长度的关系。
如果必须从裸指针进入:
unsafe fn first_byte_from_raw(ptr: *const u8, len: usize) -> Option<u8> {
if ptr.is_null() || len == 0 {
return None;
}
let data = unsafe { std::slice::from_raw_parts(ptr, len) };
data.first().copied()
}
unsafe 只出现在把裸指针转换成 slice 的地方。后面尽量回到安全 Rust,用 slice 的边界和方法处理数据。
这就是常见策略:
裸指针 / FFI / 硬件边界 -> 小 unsafe 块 -> 安全 Rust 类型
FFI 是 unsafe 的高频来源
调用 C 函数通常是 unsafe:
unsafe extern "C" {
fn strlen(s: *const std::os::raw::c_char) -> usize;
}
fn len_from_c_string(s: &std::ffi::CStr) -> usize {
unsafe { strlen(s.as_ptr()) }
}
C 函数不知道 Rust 的借用和生命周期。Rust 也无法证明 C 函数不会保存指针、不会越界、不会违反线程安全。
所以 FFI 边界要特别明确:
- 指针是否允许为空
- 字符串是否 NUL 结尾
- 内存归谁释放
- C 是否会保存指针
- 回调在哪个线程调用
- 错误码怎样返回
unsafe 只是提醒你这里有无法由 Rust 编译器验证的前提。真正的安全来自边界设计。
unsafe 不应该扩散
坏的写法是把大量业务逻辑都塞进 unsafe:
unsafe fn parse_and_handle(ptr: *const u8, len: usize) {
// parse protocol
// update state
// call callbacks
// manage resources
}
更稳的写法是先把边界转换成安全类型:
unsafe fn parse_from_raw(ptr: *const u8, len: usize) -> Result<(), ParseError> {
if ptr.is_null() {
return Err(ParseError::Null);
}
let data = unsafe { std::slice::from_raw_parts(ptr, len) };
parse_packet(data)
}
fn parse_packet(data: &[u8]) -> Result<(), ParseError> {
// safe parsing logic
Ok(())
}
这样 unsafe 只负责“从不可证明的外部世界进入 Rust 类型”。真正的解析逻辑仍然是安全代码。
这一篇先记住什么
C 和 Rust 的第一层对应关系是:
裸指针解引用 -> unsafe
C 函数调用 -> unsafe extern / unsafe call
void * + len -> unsafe 转成 &[u8] 后再安全处理
全局可变状态 -> static mut / unsafe,优先避免
文档写“调用方保证有效” -> unsafe fn 的 Safety 前提
封装好的安全接口 -> 小 unsafe 边界 + 安全 Rust API
unsafe 不是“这里不用遵守 Rust 规则”。
它表示:这里有一些编译器无法证明的前提,需要程序员保证。工程上更好的做法,是让 unsafe 尽量小、尽量靠近 FFI 或裸指针边界,然后尽快回到安全 Rust 类型。
下一篇可以进入 FFI。unsafe 是边界标记,FFI 则要具体处理 C ABI、符号导出、字符串、内存归属和错误返回。