Rust FFI 完全指南:从基础到实战
本文详细讲解 Rust 与 C 语言的交互技术,涵盖基础概念、数据类型映射、内存管理、错误处理,以及大量实际应用场景。适合 Rust 初学者和有经验的开发者。
目录
- 什么是 FFI?
- 为什么需要 FFI?
- Rust 调用 C 函数
- C 调用 Rust 函数
- 数据类型映射与转换
- 字符串处理详解
- 内存管理规则
- 错误处理
- 回调函数与闭包
- 不透明指针(Opaque Pointer)
- 工具链:bindgen 与 cbindgen
- 实战案例
- 常见陷阱与最佳实践
- 总结
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 优势
- 封装:隐藏内部实现细节
- 安全:调用者只能通过提供的函数操作
- 稳定 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 ✅ 最佳实践
- 总是使用
repr(C)处理结构体 - 在 FFI 边界捕获 panic
- 明确文档内存所有权规则
- 提供安全 Rust 封装,隐藏 unsafe
- 使用
CStr/CString处理字符串 - 使用 bindgen/cbindgen 自动生成代码
- 测试 FFI 边界,特别是错误路径
- 避免在热路径使用 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. 总结
核心要点
- FFI 是 Rust 与 C 互操作的基础,允许复用海量 C 库
#[no_mangle]+extern "C"是导出函数的标准方式unsafe是必需的,因为 Rust 无法保证外部代码的安全性- 内存管理遵循"谁分配,谁释放"原则
- 错误处理使用错误码 + 可选错误消息
- 必须捕获 panic,防止跨 FFI 边界传播
- 使用工具(bindgen, cbindgen)自动化生成代码
实际应用场景
- 数据库驱动(SQLite, PostgreSQL)
- 系统编程(Windows API, Linux syscall)
- 游戏开发(OpenGL, Vulkan)
- AI/机器学习(TensorFlow, PyTorch)
- 音视频处理(FFmpeg)
- 跨语言协作(Python, Node.js)
最终建议
FFI 是强大的工具,但也引入安全风险和复杂性。在 Rust 生态已有成熟方案时,优先使用纯 Rust 库。只有在必须调用外部 C 库或提供 C 接口时,才使用 FFI。
参考资源
全文约 4359 字,阅读大约需要 19 分钟