第十四章:Unsafe Rust与FFI——与底层对话

第十四章: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 块中,你可以:

  1. 解引用原始指针const T</code> 和 <code>mut T
  2. 调用 unsafe 函数
  3. 访问或修改可变静态变量
  4. 实现 unsafe trait
  5. 访问 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 类型说明
intc_int平台相关
longc_long平台相关
floatf32单精度浮点
doublef64双精度浮点
charc_char1字节字符
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?提示:想想底层系统编程需要什么能力。


动手实验

  1. 使用 unsafe 实现一个简单的 memcpy 函数。
  2. 封装 C 的 rand() 函数为安全的 Rust 接口。
  3. 创建一个 #[repr(C)] 结构体,并用 C 打印其大小,与 Rust 的 std::mem::size_of 对比。
  4. 解释为什么以下代码是危险的,如何修复:

``rust fn bad() -> &'static str { let s = String::from("hello"); unsafe { std::mem::transmute(&*s as &str) } } ``

← 返回目录