Rust FFI 完全指南:从基础到实战

本文详细讲解 Rust 与 C 语言的交互技术,涵盖基础概念、数据类型映射、内存管理、错误处理,以及大量实际应用场景。适合 Rust 初学者和有经验的开发者。


目录

  1. 什么是 FFI?
  2. 为什么需要 FFI?
  3. Rust 调用 C 函数
  4. C 调用 Rust 函数
  5. 数据类型映射与转换
  6. 字符串处理详解
  7. 内存管理规则
  8. 错误处理
  9. 回调函数与闭包
  10. 不透明指针(Opaque Pointer)
  11. 工具链:bindgen 与 cbindgen
  12. 实战案例
  13. 常见陷阱与最佳实践
  14. 总结

1. 什么是 FFI?

FFI (Foreign Function Interface) 是 Rust 与其它编程语言(特别是 C)交互的桥梁。它允许:

  • Rust 调用 C 函数:使用已有的 C 库(如 OpenSSL、SQLite)
  • C 调用 Rust 函数:将 Rust 代码作为库集成到 C 项目中

核心原理

┌─────────────┐         ┌─────────────┐
│   Rust 代码  │ ←─────→ │   C 代码    │
│             │   FFI   │             │
│ 安全抽象    │         │ 系统 API    │
└─────────────┘         └─────────────┘
      ↑                         ↑
      │                         │
  ABI 兼容              调用约定一致
  (内存布局)              (extern "C")

关键技术点:
- ABI 兼容:两种语言编译后的二进制接口必须一致
- 调用约定:extern "C" 使用 C 的调用约定
- 类型映射:Rust 类型与 C 类型一一对应


2. 为什么需要 FFI?

2.1 复用成熟生态

C 语言有 50 年历史,积累了海量高质量库。例如:

领域 著名 C 库 Rust 替代方案
加密 OpenSSL, libsodium ring, rustls(仍在成熟中)
数据库 SQLite, PostgreSQL rusqlite(基于 SQLite)
图形 OpenGL, Vulkan wgpu(基于 Vulkan)
音视频 FFmpeg, libav 需要大量工作
线性代数 BLAS, LAPACK ndarray(可选 BLAS 后端)

结论:重写这些库不现实,FFI 是最佳选择。

2.2 性能关键路径

科学计算、音视频编解码、网络协议栈等领域,C 库经过几十年优化,性能极致。

2.3 跨语言协作

大型项目通常多语言混合开发:
- Python 负责业务逻辑
- Rust 负责性能核心
- C 提供底层能力

2.4 与操作系统交互

Windows、Linux、macOS 的 API 都是 C 接口,必须使用 FFI。


3. Rust 调用 C 函数

3.1 基本语法

// 声明外部 C 函数
extern "C" {
    fn strlen(s: *const std::os::raw::c_char) -> usize;
    fn getpid() -> i32;
}

fn main() {
    let c_string = std::ffi::CString::new("Hello, World!").unwrap();
    
    unsafe {
        // 调用 C 函数必须在 unsafe 块中
        let len = strlen(c_string.as_ptr());
        let pid = getpid();
        
        println!("Length: {}, PID: {}", len, pid);
    }
}

关键点:
- extern "C" 指定 C 调用约定
- 调用必须在 unsafe 块中(Rust 不能保证外部函数的安全性)
- 需要手动处理字符串转换

3.2 链接 C 库

方式 1:静态链接(build.rs)

C 源码(src/math.c):

#include <stdint.h>

int32_t add(int32_t a, int32_t b) {
    return a + b;
}

int32_t multiply(int32_t a, int32_t b) {
    return a * b;
}

build.rs:

fn main() {
    // 编译 C 代码并链接
    cc::Build::new()
        .file("src/math.c")
        .compile("math");  // 生成 libmath.a
    
    // 告诉 Rust 在哪里找库
    println!("cargo:rustc-link-lib=math");
    println!("cargo:rustc-link-search=native=.");
}

Rust 调用(src/main.rs):

extern "C" {
    fn add(a: i32, b: i32) -> i32;
    fn multiply(a: i32, b: i32) -> i32;
}

fn main() {
    unsafe {
        println!("3 + 4 = {}", add(3, 4));
        println!("3 * 4 = {}", multiply(3, 4));
    }
}

方式 2:动态加载(运行时)

use libloading::{Library, Symbol};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 运行时加载动态库
    let lib = unsafe { Library::new("libmath.so") }?;
    
    // 获取函数指针
    let add: Symbol<fn(i32, i32) -> i32> = unsafe { lib.get(b"add")? };
    let result = add(10, 20);
    println!("10 + 20 = {}", result);
    
    Ok(())
}

Cargo.toml 依赖:

[dependencies]
libloading = "0.8"

3.3 系统库链接

// 链接系统库(如 OpenSSL)
extern "C" {
    fn SSL_library_init();
    fn SSL_load_error_strings();
}

fn init_openssl() {
    unsafe {
        SSL_library_init();
        SSL_load_error_strings();
    }
}

Cargo.toml:

[build-dependencies]
pkg-config = "0.3"  # 用于查找系统库


4. C 调用 Rust 函数

4.1 导出 Rust 函数

// lib.rs
use std::ffi::{CStr, CString};
use std::os::raw::c_char;

// 必须标记为 extern "C" + #[no_mangle]
// #[no_mangle] 防止函数名被 Rust 混淆
#[no_mangle]
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 {
    a + b
}

#[no_mangle]
pub extern "C" fn rust_hello(name: *const c_char) {
    if name.is_null() {
        return;
    }
    
    unsafe {
        let c_str = CStr::from_ptr(name);
        if let Ok(rust_str) = c_str.to_str() {
            println!("Hello from Rust, {}!", rust_str);
        }
    }
}

#[no_mangle]
pub extern "C" fn rust_create_message() -> *mut c_char {
    let msg = "Message from Rust".to_string();
    // 转移所有权给调用者(C 负责释放)
    CString::new(msg).unwrap().into_raw()
}

#[no_mangle]
pub extern "C" fn rust_free_string(ptr: *mut c_char) {
    if !ptr.is_null() {
        unsafe {
            // 收回所有权并释放内存
            let _ = CString::from_raw(ptr);
        }
    }
}

4.2 编译为 C 可调用的库

Cargo.toml:

[lib]
crate-type = ["staticlib"]  # 静态库
# 或 ["cdylib"]            # 动态库

编译:

cargo build --release

# 生成文件:
# Linux:   target/release/libmylib.a
# Windows: target/release/mylib.lib (静态) 或 mylib.dll (动态)
# macOS:   target/release/libmylib.a

4.3 C 程序调用

main.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 声明 Rust 函数
extern int32_t rust_add(int32_t a, int32_t b);
extern void rust_hello(const char* name);
extern char* rust_create_message(void);
extern void rust_free_string(char* ptr);

int main() {
    // 基础函数
    int result = rust_add(5, 7);
    printf("5 + 7 = %d\n", result);
    
    // 字符串参数
    rust_hello("C Programmer");
    
    // 字符串返回值(必须释放)
    char* msg = rust_create_message();
    printf("Rust says: %s\n", msg);
    rust_free_string(msg);
    
    return 0;
}

编译和链接:

# Linux
gcc -o program main.c -L target/release -lmylib -lpthread -ldl

# Windows (MinGW)
gcc -o program main.c -L target/release -lmylib -lpthread

# 运行
./program


5. 数据类型映射与转换

5.1 基础类型映射表

Rust 类型 C 类型 标准库类型 说明
i8 int8_t std::os::raw::c_schar 有符号 8 位整数
u8 uint8_t std::os::raw::c_uchar 无符号 8 位整数
i16 int16_t std::os::raw::c_short 有符号 16 位整数
u16 uint16_t std::os::raw::c_ushort 无符号 16 位整数
i32 int32_t std::os::raw::c_int 有符号 32 位整数
u32 uint32_t std::os::raw::c_uint 无符号 32 位整数
i64 int64_t std::os::raw::c_longlong 有符号 64 位整数
u64 uint64_t std::os::raw::c_ulonglong 无符号 64 位整数
f32 float - 单精度浮点数
f64 double - 双精度浮点数
bool bool (C99) - 布尔值
usize size_t std::os::raw::c_ulong 指针大小整数
*const T const T* - 常量指针
*mut T T* - 可变指针
() void - 空类型

5.2 结构体映射

C 结构体:

// mylib.h
typedef struct {
    int32_t id;
    double value;
    char name[64];
} MyData;

Rust 对应:

#[repr(C)]  // 必须!C 内存布局
#[derive(Debug, Copy, Clone)]
struct MyData {
    id: i32,
    value: f64,
    name: [std::os::raw::c_char; 64],
}

重要规则:
- 必须用 #[repr(C)] 保证内存布局与 C 一致
- 字段顺序必须与 C 结构体相同
- 数组映射为 [T; N]

5.3 联合体(Union)

C 联合体:

typedef union {
    int32_t int_val;
    double double_val;
    char string_val[32];
} MyUnion;

Rust 对应:

#[repr(C)]
union MyUnion {
    int_val: i32,
    double_val: f64,
    string_val: [std::os::raw::c_char; 32],
}

fn use_union() {
    let mut u = MyUnion { int_val: 42 };
    unsafe {
        println!("int: {}", u.int_val);
        u.double_val = 3.14;
        println!("double: {}", u.double_val);
    }
}

注意:访问联合体字段是 unsafe 操作!

5.4 枚举映射

C 枚举:

typedef enum {
    STATUS_OK = 0,
    STATUS_ERROR = 1,
    STATUS_PENDING = 2
} Status;

Rust 对应:

#[repr(C)]
#[derive(Debug)]
enum Status {
    Ok = 0,
    Error = 1,
    Pending = 2,
}

extern "C" {
    fn process_data() -> Status;
}

fn main() {
    unsafe {
        match process_data() {
            Status::Ok => println!("Success"),
            Status::Error => println!("Error occurred"),
            Status::Pending => println!("Pending..."),
        }
    }
}

5.5 函数指针

C 函数指针:

typedef void (*Callback)(int32_t data, void* user_data);
void register_callback(Callback cb, void* user_data);

Rust 对应:

type Callback = extern "C" fn(i32, *mut std::ffi::c_void);

extern "C" {
    fn register_callback(cb: Callback, user_data: *mut std::ffi::c_void);
}

// 实现回调
extern "C" fn my_callback(data: i32, user_data: *mut std::ffi::c_void) {
    println!("Callback with data: {}", data);
}

fn main() {
    unsafe {
        register_callback(my_callback, std::ptr::null_mut());
    }
}


6. 字符串处理详解

字符串是 FFI 中最容易出错的领域,需要特别注意。

6.1 C 字符串基础

  • C 字符串:以 \0 结尾的 char 数组
  • Rust 字符串:UTF-8 编码,内部不包含 \0

6.2 C → Rust(读取 C 字符串)

use std::ffi::{CStr, CString};
use std::os::raw::c_char;

extern "C" {
    fn get_c_string() -> *const c_char;  // C 返回字符串
}

fn read_c_string(ptr: *const c_char) -> Option<String> {
    if ptr.is_null() {
        return None;
    }
    
    unsafe {
        // CStr::from_ptr 假设字符串以 \0 结尾
        match CStr::from_ptr(ptr).to_str() {
            Ok(s) => Some(s.to_string()),  // 转换为 Rust String
            Err(_) => None,                // UTF-8 无效
        }
    }
}

fn main() {
    unsafe {
        let c_ptr = get_c_string();
        if let Some(s) = read_c_string(c_ptr) {
            println!("C string: {}", s);
        }
    }
}

6.3 Rust → C(传递字符串给 C)

// 方式 1:传递所有权(C 负责释放)
fn pass_string_owned(s: &str) -> *mut c_char {
    // CString::new 会复制数据并添加 \0
    CString::new(s).unwrap().into_raw()  // 转移所有权
}

// 方式 2:只读借用(Rust 保证数据存活)
fn pass_string_borrowed(s: &str) -> *const c_char {
    let c_str = CString::new(s).unwrap();
    // 注意:c_str 必须保持存活,直到 C 使用完毕
    c_str.as_ptr()  // 不转移所有权
}

extern "C" {
    fn process_string_owned(ptr: *mut c_char);
    fn process_string_borrowed(ptr: *const c_char);
}

fn main() {
    let data = "Hello from Rust".to_string();
    
    // 方式 1:传递所有权
    let ptr = pass_string_owned(&data);
    unsafe {
        process_string_owned(ptr);
        // 此时 ptr 已经被 C 释放,不能再使用
    }
    
    // 方式 2:借用(必须确保数据在调用期间存活)
    let c_str = CString::new(data).unwrap();
    unsafe {
        process_string_borrowed(c_str.as_ptr());
    }  // c_str 在这里销毁
}

6.4 接收 C 字符串并返回处理结果

#[no_mangle]
pub extern "C" fn rust_process_string(
    input: *const c_char,
    output: *mut *mut c_char,
) -> i32 {
    // 1. 检查空指针
    if input.is_null() || output.is_null() {
        return -1;
    }
    
    // 2. 读取 C 字符串
    let input_str = unsafe {
        match CStr::from_ptr(input).to_str() {
            Ok(s) => s,
            Err(_) => return -2,  // UTF-8 无效
        }
    };
    
    // 3. 处理数据
    let processed = format!("Processed: {}", input_str);
    
    // 4. 分配并返回 C 字符串
    let c_result = match CString::new(processed) {
        Ok(s) => s,
        Err(_) => return -3,  // 包含 \0
    };
    
    unsafe {
        *output = c_result.into_raw();  // 转移所有权给 C
    }
    0  // 成功
}

7. 内存管理规则

7.1 核心原则

谁分配,谁释放

分配方 释放方 做法
Rust Rust 正常,Rust 自动管理
Rust → C C Rust 用 into_raw() 转移所有权,C 调用 free()
C → Rust C Rust 只读,不释放
C → Rust Rust C 分配,Rust 读取后调用 C 的释放函数

7.2 Rust 分配内存给 C

#[no_mangle]
pub extern "C" fn create_buffer(size: usize) -> *mut u8 {
    let mut vec = Vec::with_capacity(size);
    // 假设填充数据...
    let ptr = vec.as_mut_ptr();
    std::mem::forget(vec);  // 防止 Rust 自动释放
    ptr
}

#[no_mangle]
pub extern "C" fn free_buffer(ptr: *mut u8, size: usize) {
    if !ptr.is_null() {
        unsafe {
            // 重建 Vec 并立即丢弃
            drop(Vec::from_raw_parts(ptr, size, size));
        }
    }
}

7.3 C 分配内存给 Rust

extern "C" {
    fn c_alloc(size: usize) -> *mut u8;
    fn c_free(ptr: *mut u8);
}

fn use_c_memory() -> Result<Vec<u8>, Box<dyn std::error::Error>> {
    let ptr = unsafe { c_alloc(1024) };
    if ptr.is_null() {
        return Err("Allocation failed".into());
    }
    
    // 读取数据...
    let data = unsafe { Vec::from_raw_parts(ptr, 1024, 1024) };
    // 注意:这会接管内存,需要确保使用正确的释放方式
    
    // 如果 C 需要释放,不能使用 Vec::from_raw_parts
    // 应该:
    unsafe {
        // 读取数据到 Rust Vec
        let slice = std::slice::from_raw_parts(ptr, 1024);
        let mut rust_vec = slice.to_vec();
        
        // 让 C 释放内存
        c_free(ptr);
        
        Ok(rust_vec)
    }
}

7.4 内存泄漏避免

❌ 错误示例:

#[no_mangle]
pub extern "C" fn get_string() -> *mut c_char {
    let s = String::from("Hello");
    let c_str = CString::new(s).unwrap();
    c_str.into_raw()  // 转移所有权
    // 调用者必须释放,否则内存泄漏!
}

✅ 正确做法 - 提供释放函数:

#[no_mangle]
pub extern "C" fn get_string() -> *mut c_char {
    let s = String::from("Hello");
    CString::new(s).unwrap().into_raw()
}

#[no_mangle]
pub extern "C" fn free_string(ptr: *mut c_char) {
    if !ptr.is_null() {
        unsafe { drop(CString::from_raw(ptr)); }
    }
}


8. 错误处理

8.1 错误码模式

最常见的 FFI 错误处理方式:

#[repr(C)]
pub enum FfiError {
    Success = 0,
    InvalidInput = 1,
    NotFound = 2,
    Internal = 3,
    OutOfMemory = 4,
}

#[no_mangle]
pub extern "C" fn process_data(
    input: *const c_char,
    output: *mut *mut c_char,
) -> FfiError {
    // 1. 空指针检查
    if input.is_null() || output.is_null() {
        return FfiError::InvalidInput;
    }
    
    // 2. 字符串转换
    let input_str = match unsafe { CStr::from_ptr(input).to_str() } {
        Ok(s) => s,
        Err(_) => return FfiError::InvalidInput,
    };
    
    // 3. 业务逻辑
    let result = match inner_process(input_str) {
        Ok(data) => data,
        Err(e) => return match e {
            MyError::NotFound => FfiError::NotFound,
            MyError::Internal => FfiError::Internal,
        }
    };
    
    // 4. 分配结果
    let c_result = match CString::new(result) {
        Ok(s) => s,
        Err(_) => return FfiError::OutOfMemory,
    };
    
    unsafe { *output = c_result.into_raw(); }
    FfiError::Success
}

enum MyError {
    NotFound,
    Internal,
}

fn inner_process(input: &str) -> Result<String, MyError> {
    if input.is_empty() {
        return Err(MyError::NotFound);
    }
    Ok(format!("Processed: {}", input))
}

8.2 错误消息 + 错误码

#[repr(C)]
pub struct FfiResult {
    code: i32,
    message: *mut c_char,  // 错误消息(需要释放)
}

#[no_mangle]
pub extern "C" fn operation_with_error() -> FfiResult {
    let result = do_something();
    match result {
        Ok(data) => FfiResult {
            code: 0,
            message: std::ptr::null_mut(),
        },
        Err(e) => {
            let msg = CString::new(e.to_string()).unwrap();
            FfiResult {
                code: e.code(),
                message: msg.into_raw(),
            }
        }
    }
}

#[no_mangle]
pub extern "C" fn free_result(result: FfiResult) {
    if !result.message.is_null() {
        unsafe { drop(CString::from_raw(result.message)); }
    }
}

8.3 捕获 Panic

重要:Rust 的 panic 不能跨 FFI 边界!

#[no_mangle]
pub extern "C" fn safe_ffi_function() -> i32 {
    let result = std::panic::catch_unwind(|| {
        // 可能 panic 的代码
        dangerous_operation()
    });
    
    match result {
        Ok(val) => val,
        Err(_) => {
            // 记录错误日志,但不传播 panic
            eprintln!("Panic caught in FFI boundary");
            -1  // 返回错误码
        }
    }
}

为什么必须捕获?
- C 无法处理 Rust 的 unwinding
- 跨 FFI 边界 panic 是未定义行为
- 可能导致程序崩溃或内存泄漏


9. 回调函数与闭包

9.1 简单回调

C 函数定义:

// callback.h
typedef void (*Callback)(int32_t value);
void execute_with_callback(Callback cb);

Rust 实现:

type Callback = extern "C" fn(i32);

extern "C" {
    fn execute_with_callback(cb: Callback);
}

// 定义回调函数
extern "C" fn my_callback(value: i32) {
    println!("Callback received: {}", value);
}

fn main() {
    unsafe {
        execute_with_callback(my_callback);
    }
}

9.2 带用户数据的回调

C 函数定义:

typedef void (*Callback)(int32_t value, void* user_data);
void execute_with_data(Callback cb, void* user_data);

Rust 实现:

type Callback = extern "C" fn(i32, *mut std::ffi::c_void);

extern "C" {
    fn execute_with_data(cb: Callback, user_data: *mut std::ffi::c_void);
}

// 用户数据结构
struct UserData {
    name: String,
    counter: i32,
}

// 回调实现
extern "C" fn my_callback(value: i32, data: *mut std::ffi::c_void) {
    if data.is_null() {
        return;
    }
    
    unsafe {
        let user_data = &mut *(data as *mut UserData);
        user_data.counter += value;
        println!("{}: {}", user_data.name, user_data.counter);
    }
}

fn main() {
    let mut data = Box::new(UserData {
        name: "Counter".to_string(),
        counter: 0,
    });
    
    let ptr = Box::into_raw(data) as *mut std::ffi::c_void;
    
    unsafe {
        execute_with_data(my_callback, ptr);
        // 使用完后清理
        drop(Box::from_raw(ptr as *mut UserData));
    }
}

9.3 闭包作为回调

由于 Rust 闭包与 C 函数指针不兼容,需要一层包装:

use std::sync::{Arc, Mutex};

struct CallbackWrapper {
    func: Box<dyn FnMut(i32) -> i32 + Send>,
}

extern "C" fn c_callback(value: i32, data: *mut std::ffi::c_void) -> i32 {
    if data.is_null() {
        return -1;
    }
    
    unsafe {
        let wrapper = &mut *(data as *mut CallbackWrapper);
        (wrapper.func)(value)
    }
}

fn register_rust_callback<F>(mut f: F) 
where 
    F: FnMut(i32) -> i32 + Send + 'static,
{
    let wrapper = Box::new(CallbackWrapper {
        func: Box::new(f),
    });
    
    let ptr = Box::into_raw(wrapper) as *mut std::ffi::c_void;
    unsafe {
        // 注册回调
        register_callback(c_callback, ptr);
    }
}

10. 不透明指针(Opaque Pointer)

10.1 概念

不透明指针用于隐藏内部数据结构,只暴露指针给调用者。

C 头文件(mylib.h):

// 前向声明,结构体定义在 C 文件内部
typedef struct MyContext MyContext;

MyContext* my_context_new(void);
void my_context_process(MyContext* ctx, int value);
void my_context_free(MyContext* ctx);

Rust 实现:

// 内部结构体定义
pub struct MyContext {
    data: Vec<i32>,
    config: MyConfig,
}

struct MyConfig {
    mode: u8,
    threshold: f64,
}

// 导出函数
#[no_mangle]
pub extern "C" fn my_context_new() -> *mut MyContext {
    let ctx = MyContext {
        data: Vec::new(),
        config: MyConfig {
            mode: 1,
            threshold: 0.5,
        },
    };
    Box::into_raw(Box::new(ctx))  // 转换为裸指针
}

#[no_mangle]
pub extern "C" fn my_context_process(ctx: *mut MyContext, value: i32) {
    if ctx.is_null() {
        return;
    }
    unsafe {
        let ctx = &mut *ctx;
        ctx.data.push(value);
        // 处理数据...
    }
}

#[no_mangle]
pub extern "C" fn my_context_free(ctx: *mut MyContext) {
    if !ctx.is_null() {
        unsafe {
            // 使用 Box::from_raw 回收内存
            drop(Box::from_raw(ctx));
        }
    }
}

10.2 优势

  1. 封装:隐藏内部实现细节
  2. 安全:调用者只能通过提供的函数操作
  3. 稳定 ABI:内部结构变化不影响外部接口

11. 工具链:bindgen 与 cbindgen

11.1 bindgen:自动生成 Rust 绑定

用途:从 C 头文件自动生成 Rust FFI 代码。

安装:

cargo install bindgen-cli

使用示例:

C 头文件(wrapper.h):

#include <stdint.h>

typedef struct {
    int32_t id;
    double value;
    char name[64];
} MyStruct;

int32_t process_struct(const MyStruct* data);
void free_struct(MyStruct* data);

build.rs:

use bindgen;

fn main() {
    // 重新生成条件
    println!("cargo:rerun-if-changed=wrapper.h");
    
    // 生成绑定
    let bindings = bindgen::Builder::default()
        .header("wrapper.h")
        .parse_callbacks(Box::new(bindgen::CargoCallbacks))
        .generate()
        .expect("Unable to generate bindings");
    
    // 写入文件
    bindings
        .write_to_file("src/bindings.rs")
        .expect("Couldn't write bindings");
}

Rust 使用:

// 包含生成的绑定
include!("bindings.rs");

fn main() {
    let mut data = MyStruct {
        id: 42,
        value: 3.14,
        name: [0; 64],
    };
    
    unsafe {
        let result = process_struct(&mut data);
        println!("Result: {}", result);
    }
}

11.2 cbindgen:生成 C 头文件

用途:从 Rust 代码生成 C 头文件。

安装:

cargo install cbindgen

使用:

# 生成头文件
cbindgen --lang c --output mylib.h

# 指定 crate
cbindgen --crate mylib --output mylib.h

生成示例:

Rust 代码:

#[repr(C)]
pub struct MyData {
    pub id: i32,
    pub value: f64,
}

#[no_mangle]
pub extern "C" fn process_data(data: *const MyData) -> i32 {
    // ...
    0
}

生成的 C 头文件:

#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>

typedef struct {
  int32_t id;
  double value;
} MyData;

int32_t process_data(const MyData *data);

11.3 最佳实践:完整工作流

┌─────────────┐
│  C 头文件   │
│ (wrapper.h) │
└──────┬──────┘
       │ bindgen
       ▼
┌─────────────┐
│ Rust 绑定   │
│ (bindings.rs)│
└──────┬──────┘
       │ 使用
       ▼
┌─────────────┐
│ Rust 代码   │
│ (lib.rs)    │
└──────┬──────┘
       │ cbindgen
       ▼
┌─────────────┐
│ C 头文件    │
│ (mylib.h)   │
└─────────────┘

12. 实战案例

12.1 调用 SQLite

use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::ptr;

// SQLite 类型定义
#[repr(C)]
struct sqlite3;

// SQLite 函数声明
extern "C" {
    fn sqlite3_open(
        filename: *const c_char,
        ppDb: *mut *mut sqlite3
    ) -> i32;
    
    fn sqlite3_exec(
        db: *mut sqlite3,
        sql: *const c_char,
        callback: Option<extern "C" fn(*mut c_char, i32, *mut *mut c_char, *mut *mut c_char) -> i32>,
        callback_arg: *mut c_char,
        errmsg: *mut *mut c_char
    ) -> i32;
    
    fn sqlite3_close(db: *mut sqlite3) -> i32;
    fn sqlite3_free(ptr: *mut c_char);
}

// 安全封装
pub struct SqliteConnection {
    db: *mut sqlite3,
}

impl SqliteConnection {
    pub fn open(filename: &str) -> Result<Self, String> {
        let filename_c = CString::new(filename).unwrap();
        let mut db = ptr::null_mut();
        
        unsafe {
            let rc = sqlite3_open(filename_c.as_ptr(), &mut db);
            if rc != 0 {
                return Err("Failed to open database".to_string());
            }
            Ok(SqliteConnection { db })
        }
    }
    
    pub fn execute(&self, sql: &str) -> Result<(), String> {
        let sql_c = CString::new(sql).unwrap();
        let mut errmsg = ptr::null_mut();
        
        unsafe {
            let rc = sqlite3_exec(
                self.db,
                sql_c.as_ptr(),
                None,
                ptr::null_mut(),
                &mut errmsg
            );
            
            if rc != 0 {
                let err = CStr::from_ptr(errmsg).to_string_lossy().into_owned();
                sqlite3_free(errmsg);
                return Err(err);
            }
            Ok(())
        }
    }
}

impl Drop for SqliteConnection {
    fn drop(&mut self) {
        unsafe {
            sqlite3_close(self.db);
        }
    }
}

// 使用
fn main() -> Result<(), String> {
    let conn = SqliteConnection::open("test.db")?;
    conn.execute("CREATE TABLE users (id INTEGER, name TEXT)")?;
    conn.execute("INSERT INTO users VALUES (1, 'Alice')")?;
    println!("Success!");
    Ok(())
}

12.2 Python 调用 Rust 库(PyO3)

Rust 代码(src/lib.rs):

use pyo3::prelude::*;

#[pyfunction]
fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[pyfunction]
fn process_string(s: &str) -> String {
    format!("Processed: {}", s)
}

#[pymodule]
fn my_rust_lib(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(add, m)?)?;
    m.add_function(wrap_pyfunction!(process_string, m)?)?;
    Ok(())
}

Cargo.toml:

[lib]
crate-type = ["cdylib"]

[dependencies]
pyo3 = { version = "0.21", features = ["extension-module"] }

编译:

maturin build  # 或 pyo3-pack build

Python 使用:

import my_rust_lib

result = my_rust_lib.add(5, 3)
print(result)  # 8

text = my_rust_lib.process_string("Hello")
print(text)  # Processed: Hello

12.3 高性能矩阵运算(调用 BLAS)

use std::ffi::CString;

// BLAS 函数声明
extern "C" {
    fn cblas_dgemm(
        layout: i32,
        transa: i32,
        transb: i32,
        m: i32,
        n: i32,
        k: i32,
        alpha: f64,
        a: *const f64,
        lda: i32,
        b: *const f64,
        ldb: i32,
        beta: f64,
        c: *mut f64,
        ldc: i32,
    );
}

// 安全封装
pub fn matrix_multiply(
    a: &[f64],
    b: &[f64],
    rows: usize,
    cols: usize,
) -> Vec<f64> {
    // 使用 OpenBLAS 进行高性能矩阵乘法
    let n = rows * cols;
    let mut result = vec![0.0; n];
    
    unsafe {
        cblas_dgemm(
            101,  // CblasRowMajor
            111,  // CblasNoTrans
            111,  // CblasNoTrans
            rows as i32,
            cols as i32,
            rows as i32,
            1.0,
            a.as_ptr(),
            rows as i32,
            b.as_ptr(),
            rows as i32,
            0.0,
            result.as_mut_ptr(),
            rows as i32,
        );
    }
    
    result
}

fn main() {
    let a = vec![1.0, 2.0, 3.0, 4.0];
    let b = vec![2.0, 3.0, 4.0, 5.0];
    let result = matrix_multiply(&a, &b, 2, 2);
    println!("{:?}", result);  // 高性能矩阵乘法结果
}

13. 常见陷阱与最佳实践

13.1 ❌ 常见陷阱

1. 忘记 #[no_mangle]

// ❌ 错误:函数名会被混淆
pub extern "C" fn my_function() { }

// ✅ 正确
#[no_mangle]
pub extern "C" fn my_function() { }

2. 跨 FFI 边界 panic

// ❌ 错误
#[no_mangle]
pub extern "C" fn bad_function() {
    panic!("This will cause UB!");  // C 无法处理
}

// ✅ 正确
#[no_mangle]
pub extern "C" fn good_function() -> i32 {
    std::panic::catch_unwind(|| {
        // 可能 panic 的代码
    }).unwrap_or(-1)
}

3. 内存泄漏

// ❌ 错误:忘记释放
#[no_mangle]
pub extern "C" fn get_string() -> *mut c_char {
    CString::new("Hello").unwrap().into_raw()
    // 调用者不知道要释放!
}

// ✅ 正确:提供释放函数
#[no_mangle]
pub extern "C" fn get_string() -> *mut c_char {
    CString::new("Hello").unwrap().into_raw()
}

#[no_mangle]
pub extern "C" fn free_string(ptr: *mut c_char) {
    if !ptr.is_null() {
        unsafe { drop(CString::from_raw(ptr)); }
    }
}

4. 生命周期错误

// ❌ 错误:返回局部变量的指针
#[no_mangle]
pub extern "C" fn bad_return() -> *const c_char {
    let s = CString::new("Hello").unwrap();
    s.as_ptr()  // s 离开作用域后释放
}

// ✅ 正确:使用 into_raw 转移所有权
#[no_mangle]
pub extern "C" fn good_return() -> *mut c_char {
    CString::new("Hello").unwrap().into_raw()
}

5. 结构体布局不一致

// ❌ 错误:Rust 默认布局与 C 不同
struct Point {
    x: f64,
    y: f64,
}

// ✅ 正确
#[repr(C)]
struct Point {
    x: f64,
    y: f64,
}

13.2 ✅ 最佳实践

  1. 总是使用 repr(C) 处理结构体
  2. 在 FFI 边界捕获 panic
  3. 明确文档内存所有权规则
  4. 提供安全 Rust 封装,隐藏 unsafe
  5. 使用 CStr/CString 处理字符串
  6. 使用 bindgen/cbindgen 自动生成代码
  7. 测试 FFI 边界,特别是错误路径
  8. 避免在热路径使用 FFI(如果有性能要求)

13.3 安全封装模式

// 底层 FFI(unsafe)
mod ffi {
    use std::os::raw::*;
    
    extern "C" {
        pub fn c_function(input: *const c_char) -> *mut c_char;
        pub fn c_free(ptr: *mut c_char);
    }
}

// 安全封装
pub fn safe_function(input: &str) -> Result<String, Box<dyn std::error::Error>> {
    let input_c = std::ffi::CString::new(input)?;
    let result_ptr = unsafe {
        ffi::c_function(input_c.as_ptr())
    };
    
    if result_ptr.is_null() {
        return Err("Function returned null".into());
    }
    
    let result = unsafe {
        let s = std::ffi::CStr::from_ptr(result_ptr).to_str()?;
        let owned = s.to_string();
        ffi::c_free(result_ptr);
        owned
    };
    
    Ok(result)
}

14. 总结

核心要点

  1. FFI 是 Rust 与 C 互操作的基础,允许复用海量 C 库
  2. #[no_mangle] + extern "C" 是导出函数的标准方式
  3. unsafe 是必需的,因为 Rust 无法保证外部代码的安全性
  4. 内存管理遵循"谁分配,谁释放"原则
  5. 错误处理使用错误码 + 可选错误消息
  6. 必须捕获 panic,防止跨 FFI 边界传播
  7. 使用工具(bindgen, cbindgen)自动化生成代码

实际应用场景

  • 数据库驱动(SQLite, PostgreSQL)
  • 系统编程(Windows API, Linux syscall)
  • 游戏开发(OpenGL, Vulkan)
  • AI/机器学习(TensorFlow, PyTorch)
  • 音视频处理(FFmpeg)
  • 跨语言协作(Python, Node.js)

最终建议

FFI 是强大的工具,但也引入安全风险和复杂性。在 Rust 生态已有成熟方案时,优先使用纯 Rust 库。只有在必须调用外部 C 库或提供 C 接口时,才使用 FFI。


参考资源

  • Rust FFI 官方文档
  • bindgen 文档
  • cbindgen 文档
  • PyO3 文档
  • FFI 安全指南

全文约 4359 字,阅读大约需要 19 分钟

二维码生成中...

扫码打开当前链接

Powered by MD2WE