第十四章:Unsafe Rust与FFI——与底层对话
本章导读:Rust 的安全保证令人赞叹,但有时我们需要跨越这道安全屏障。也许是调用 C 语言库、操作硬件内存、或实现底层原语。Unsafe Rust 就是这样的"后门"——它允许你暂时关闭安全检查,但也意味着你必须手动维护安全。正如蜘蛛侠所言:"能力越大,责任越大。"
⚠️ 14.1 什么是 Unsafe?
🚪 14.1.1 Unsafe 块
unsafe 块告诉编译器:"我知道自己在做什么,请允许我执行危险操作"。
fn main() {
let mut num = 5;
// 普通代码不能修改不可变引用
// let r = #
// *r = 10; // 错误!
// unsafe 块中可以绕过某些规则
let r = &mut num as *mut i32;
unsafe {
*r = 10; // 解引用原始指针
}
println!("num = {}", num); // 10
}
🎯 14.1.2 Unsafe 允许的操作
在 unsafe 块中,你可以:
- 解引用原始指针(
const T</code> 和 <code>mut T) - 调用 unsafe 函数
- 访问或修改可变静态变量
- 实现 unsafe trait
- 访问 union 字段
重要:unsafe 不会关闭借用检查器!它只是允许上述5种额外操作。
fn main() {
let x = 5;
let r = &x as *const i32; // 创建原始指针(安全)
unsafe {
println!("r 指向的值: {}", *r); // 解引用(需要 unsafe)
}
}
🔗 14.2 原始指针
📍 14.2.1 两种原始指针
// *const T: 不可变原始指针
// *mut T: 可变原始指针
let mut value = 42;
// 从引用创建
let const_ptr: *const i32 = &value as *const i32;
let mut_ptr: *mut i32 = &mut value as *mut i32;
// 直接从地址创建(危险!)
let addr = 0x7fff5fbff8c0; // 假设的内存地址
let ptr = addr as *const i32;
// 智能指针转换为原始指针
let boxed = Box::new(100);
let box_ptr: *const i32 = Box::into_raw(boxed);
// 使用后记得回收
unsafe {
Box::from_raw(box_ptr as *mut i32); // 重新装箱以释放内存
}
📏 14.2.2 指针运算
fn main() {
let arr = [1, 2, 3, 4, 5];
let ptr = arr.as_ptr();
unsafe {
// 指针偏移
println!("第一个: {}", *ptr);
println!("第二个: {}", *ptr.add(1));
println!("第三个: {}", *ptr.add(2));
// 使用 offset
let third = ptr.offset(2);
println!("第三个 (offset): {}", *third);
}
}
🔄 14.2.3 指针与引用转换
fn main() {
let mut data = vec![1, 2, 3, 4, 5];
// 引用 -> 原始指针
let ptr = data.as_mut_ptr();
unsafe {
// 原始指针 -> 引用(需要确保有效性)
let slice = std::slice::from_raw_parts(ptr, 5);
println!("切片: {:?}", slice);
// 修改数据
*ptr.add(0) = 100;
println!("修改后: {:?}", data);
}
}
📞 14.3 FFI:调用 C 代码
FFI(Foreign Function Interface)允许 Rust 调用其他语言的代码。
🌍 14.3.1 调用 C 标准库
// 链接到 C 标准库
extern "C" {
// 声明外部函数
fn abs(input: i32) -> i32;
fn strlen(s: *const i8) -> usize;
}
fn main() {
unsafe {
// 调用 C 函数
let x = -5;
println!("abs({}) = {}", x, abs(x));
// 使用 C 字符串
let s = std::ffi::CString::new("Hello").unwrap();
println!("长度: {}", strlen(s.as_ptr()));
}
}
📚 14.3.2 调用自定义 C 代码
首先创建 C 代码:
// example.c
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
void print_message(const char* msg) {
printf("C says: %s\n", msg);
}
然后在 Rust 中调用:
// build.rs(构建脚本)
fn main() {
cc::Build::new()
.file("src/example.c")
.compile("example");
}
// main.rs
extern "C" {
fn add(a: i32, b: i32) -> i32;
fn print_message(msg: *const i8);
}
fn main() {
unsafe {
let result = add(3, 4);
println!("3 + 4 = {}", result);
let msg = std::ffi::CString::new("Hello from Rust!").unwrap();
print_message(msg.as_ptr());
}
}
📦 14.3.3 Cargo.toml 配置
[package]
name = "ffi_example"
version = "0.1.0"
edition = "2021"
[build-dependencies]
cc = "1.0"
🔄 14.4 从 C 调用 Rust
让 C 代码调用 Rust 函数。
🎤 14.4.1 导出 Rust 函数
// lib.rs
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
/// Rust 函数导出给 C 使用
#[no_mangle] // 防止名称修饰
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 {
a + b
}
/// 处理 C 字符串
#[no_mangle]
pub extern "C" fn rust_greet(name: *const c_char) -> *mut c_char {
unsafe {
// 将 C 字符串转为 Rust 字符串
let name_cstr = CStr::from_ptr(name);
let name_str = name_cstr.to_str().unwrap_or("Unknown");
// 构建问候语
let greeting = format!("Hello, {}!", name_str);
// 转回 C 字符串
CString::new(greeting).unwrap().into_raw()
}
}
/// 释放 Rust 分配的字符串
#[no_mangle]
pub extern "C" fn rust_free_string(s: *mut c_char) {
unsafe {
if !s.is_null() {
let _ = CString::from_raw(s);
}
}
}
📋 14.4.2 生成头文件
使用 cbindgen 自动生成 C 头文件:
cargo install cbindgen
cbindgen --crate ffi_example --output ffi_example.h
生成的头文件:
// ffi_example.h
#pragma once
#include <stdint.h>
int32_t rust_add(int32_t a, int32_t b);
char *rust_greet(const char *name);
void rust_free_string(char *s);
🔗 14.4.3 C 代码调用
// main.c
#include <stdio.h>
#include "ffi_example.h"
int main() {
int result = rust_add(10, 20);
printf("10 + 20 = %d\n", result);
char* greeting = rust_greet("Rustacean");
printf("%s\n", greeting);
rust_free_string(greeting);
return 0;
}
🔧 14.5 类型映射
Rust 和 C 的类型对应关系:
📊 14.5.1 原始类型
| C 类型 | Rust 类型 | 说明 |
|---|---|---|
int | c_int | 平台相关 |
long | c_long | 平台相关 |
float | f32 | 单精度浮点 |
double | f64 | 双精度浮点 |
char | c_char | 1字节字符 |
void* | *mut c_void | 通用指针 |
📝 14.5.2 字符串处理
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
// Rust 字符串 -> C 字符串
fn rust_to_c(s: &str) -> CString {
CString::new(s).expect("CString::new failed")
}
// C 字符串 -> Rust 字符串
unsafe fn c_to_rust(ptr: *const c_char) -> &str {
CStr::from_ptr(ptr).to_str().unwrap_or("")
}
fn main() {
let rust_str = "Hello, C!";
// 转换为 C 字符串
let c_string = rust_to_c(rust_str);
let c_ptr = c_string.as_ptr();
unsafe {
// 转回 Rust 字符串
let back = c_to_rust(c_ptr);
println!("往返: {}", back);
}
}
📦 14.5.3 结构体
use std::os::raw::{c_int, c_double};
// 使用 #[repr(C)] 保证内存布局与 C 兼容
#[repr(C)]
pub struct Point {
pub x: c_double,
pub y: c_double,
}
#[repr(C)]
pub struct Rectangle {
pub top_left: Point,
pub bottom_right: Point,
}
#[no_mangle]
pub extern "C" fn create_point(x: c_double, y: c_double) -> Point {
Point { x, y }
}
#[no_mangle]
pub extern "C" fn point_distance(p1: Point, p2: Point) -> c_double {
let dx = p2.x - p1.x;
let dy = p2.y - p1.y;
(dx * dx + dy * dy).sqrt()
}
🛡️ 14.6 Unsafe 的安全抽象
好的 Rust 代码应该将 unsafe 封装在安全接口后面。
📦 14.6.1 封装原则
use std::ptr::NonNull;
/// 自定义向量,内部使用 unsafe,但对外安全
pub struct SafeVec<T> {
ptr: NonNull<T>,
len: usize,
capacity: usize,
}
impl<T> SafeVec<T> {
pub fn new() -> Self {
Self {
ptr: NonNull::dangling(),
len: 0,
capacity: 0,
}
}
pub fn push(&mut self, item: T) {
if self.len == self.capacity {
self.grow();
}
unsafe {
// 安全:我们确保有足够空间
std::ptr::write(self.ptr.as_ptr().add(self.len), item);
}
self.len += 1;
}
pub fn get(&self, index: usize) -> Option<&T> {
if index < self.len {
unsafe {
// 安全:index 已检查
Some(&*self.ptr.as_ptr().add(index))
}
} else {
None
}
}
fn grow(&mut self) {
let new_capacity = if self.capacity == 0 { 1 } else { self.capacity * 2 };
let new_layout = std::alloc::Layout::array::<T>(new_capacity).unwrap();
let new_ptr = if self.capacity == 0 {
unsafe { std::alloc::alloc(new_layout) as *mut T }
} else {
let old_layout = std::alloc::Layout::array::<T>(self.capacity).unwrap();
unsafe {
std::alloc::realloc(
self.ptr.as_ptr() as *mut u8,
old_layout,
new_layout.size(),
) as *mut T
}
};
self.ptr = NonNull::new(new_ptr).expect("allocation failed");
self.capacity = new_capacity;
}
}
impl<T> Drop for SafeVec<T> {
fn drop(&mut self) {
if self.capacity > 0 {
// 先 drop 所有元素
for i in 0..self.len {
unsafe {
std::ptr::drop_in_place(self.ptr.as_ptr().add(i));
}
}
// 释放内存
let layout = std::alloc::Layout::array::<T>(self.capacity).unwrap();
unsafe {
std::alloc::dealloc(self.ptr.as_ptr() as *mut u8, layout);
}
}
}
}
🎯 14.6.2 不变量文档化
/// 分割字符串为两半
///
/// # Safety
///
/// 调用者必须确保:
/// - `s` 是有效的 UTF-8 字符串
/// - `mid` 在字符串的有效边界上
unsafe fn split_at_unchecked(s: &str, mid: usize) -> (&str, &str) {
let len = s.len();
let ptr = s.as_ptr();
(
std::str::from_utf8_unchecked(std::slice::from_raw_parts(ptr, mid)),
std::str::from_utf8_unchecked(std::slice::from_raw_parts(ptr.add(mid), len - mid)),
)
}
fn split_at(s: &str, mid: usize) -> (&str, &str) {
// 安全检查
assert!(mid <= s.len());
assert!(s.is_char_boundary(mid));
// 安全封装
unsafe { split_at_unchecked(s, mid) }
}
🧪 14.7 实战:封装 C 库
让我们封装一个简单的 C 哈希库:
// simple_hash.c
#include <stdint.h>
uint32_t simple_hash(const char* str) {
uint32_t hash = 5381;
int c;
while ((c = *str++)) {
hash = ((hash << 5) + hash) + c;
}
return hash;
}
// lib.rs
use std::ffi::CString;
use std::os::raw::c_char;
extern "C" {
fn simple_hash(str: *const c_char) -> u32;
}
/// 安全的哈希计算接口
pub fn compute_hash(input: &str) -> Result<u32, std::ffi::NulError> {
let c_string = CString::new(input)?;
unsafe {
// 安全:c_string 是有效的,且在调用期间存活
Ok(simple_hash(c_string.as_ptr()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hash() {
let h1 = compute_hash("hello").unwrap();
let h2 = compute_hash("hello").unwrap();
let h3 = compute_hash("world").unwrap();
assert_eq!(h1, h2); // 相同输入 = 相同输出
assert_ne!(h1, h3); // 不同输入 = 不同输出
}
}
⚠️ 14.8 Undefined Behavior
Unsafe 代码可能触发未定义行为(UB),这是最危险的情况。
💣 14.8.1 常见的 UB 来源
// ❌ 使用未初始化的内存
fn bad_uninit() {
let x: i32;
println!("{}", x); // UB!
}
// ❌ 空指针解引用
fn bad_null() {
let ptr: *const i32 = std::ptr::null();
unsafe {
println!("{}", *ptr); // UB!
}
}
// ❌ 悬垂指针
fn bad_dangling() {
let ptr: *const i32;
{
let x = 5;
ptr = &x;
}
unsafe {
println!("{}", *ptr); // UB! x 已被释放
}
}
// ❌ 数据竞争
fn bad_race() {
use std::sync::atomic::{AtomicUsize, Ordering};
let data = AtomicUsize::new(0);
// 同一变量的非原子访问与原子访问同时发生
// 这是 UB
}
🛠️ 14.8.2 避免UB的技巧
// ✅ 初始化内存
fn good_init() {
let x: i32 = 0; // 明确初始化
// 或使用 MaybeUninit
let mut x: std::mem::MaybeUninit<i32> = std::mem::MaybeUninit::uninit();
unsafe {
x.as_mut_ptr().write(42);
let x = x.assume_init(); // 现在安全了
}
}
// ✅ 检查空指针
fn good_null_check(ptr: *const i32) {
if ptr.is_null() {
println!("空指针");
return;
}
unsafe {
println!("{}", *ptr);
}
}
// ✅ 确保生命周期
fn good_lifetime<'a>(data: &'a Vec<i32>) -> *const i32 {
data.as_ptr() // 指针与 data 生命周期绑定
}
📝 本章小结
本章我们探索了 Unsafe Rust 和 FFI:
| 概念 | 说明 |
|---|---|
unsafe 块 | 允许5种危险操作 |
| 原始指针 | const T</code> 和 <code>mut T |
extern "C" | 声明外部函数 |
#[no_mangle] | 导出函数给 C |
#[repr(C)] | C 兼容内存布局 |
关键原则:
- 最小化 unsafe 代码范围
- 用安全接口封装 unsafe 实现
- 文档化所有不变量
- 理解 UB 并避免它
费曼技巧提问:为什么 Rust 不完全禁止 unsafe?提示:想想底层系统编程需要什么能力。
动手实验:
- 使用 unsafe 实现一个简单的
memcpy函数。- 封装 C 的
rand()函数为安全的 Rust 接口。- 创建一个
#[repr(C)]结构体,并用 C 打印其大小,与 Rust 的std::mem::size_of对比。- 解释为什么以下代码是危险的,如何修复:
``
rust fn bad() -> &'static str { let s = String::from("hello"); unsafe { std::mem::transmute(&*s as &str) } }``