首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >36-Rust 教程 - FFI

36-Rust 教程 - FFI

作者头像
LarryLan
发布2026-06-05 20:27:49
发布2026-06-05 20:27:49
70
举报

FFI

当 Rust 遇上 C:一场跨语言的"相亲",安全与自由的碰撞

🎬 引入

想象一下这个场景:

你是一个 Rust 程序员,代码写得风生水起,内存安全、并发无忧。突然有一天,老板走过来:

"咱们有个老的 C 库,性能特别好,你能不能把它集成到咱们的 Rust 项目里?"

你心里一紧:"Rust 和 C……这俩能玩到一起吗?"

答案是:能!而且玩得还不错!

这就是咱们今天要聊的 FFI(Foreign Function Interface,外部函数接口)。简单说,就是让 Rust 和 C(以及其他语言)能够互相调用对方的代码。

但先别急着高兴。Rust 和 C 就像两个性格迥异的人:

  • Rust:严谨、安全、处处检查
  • C:自由、奔放、"我说了算"

让它们合作,就像让一个强迫症和一个随性的人一起做饭——需要一些规则和技巧。

今天,咱们就来学习怎么让这两位"握手言和"。

📌 核心概念:FFI 是什么?

FFI 的定义

FFI(Foreign Function Interface)允许一种语言调用另一种语言的函数。在 Rust 中,主要指 Rust 调用 CC 调用 Rust

为什么需要 FFI?

  1. 复用现有代码:几十年积累的 C 库,不能扔了吧?
  2. 性能关键代码:某些底层操作 C 确实更快
  3. 系统 API:操作系统 API 大多是 C 接口
  4. 生态互补:某些功能 C 生态更成熟

Rust 和 C 的主要差异

特性

Rust

C

内存安全

编译时保证

程序员负责

空指针

Option

NULL 指针

字符串

UTF-8,有长度

以\0结尾的字节数组

错误处理

Result<T, E>

返回值/errno

命名约定

snake_case

各种风格都有

关键问题: 这些差异需要在 FFI 边界处进行转换。

💻 代码示例

1. Rust 调用 C 函数

这是最常见的场景。假设我们有一个 C 库:

代码语言:javascript
复制
// math_lib.c
#include <stdint.h>

// 简单的加法函数
int32_t c_add(int32_t a, int32_t b) {
    return a + b;
}

// 字符串处理(注意:C 字符串是\0结尾的)
int c_string_length(const char* str) {
    int len = ;
    while (str[len] != '\0') {
        len++;
    }
    return len;
}

编译成动态库:

代码语言:javascript
复制
# Linux/macOS
gcc -shared -fPIC -o libmath_lib.so math_lib.c

# Windows
gcc -shared -o math_lib.dll math_lib.c

在 Rust 中调用:

代码语言:javascript
复制
// main.rs

// 声明外部函数
extern "C" {
    fn c_add(a: i32, b: i32) -> i32;
    fn c_string_length(str: *const u8) -> i32;
}

fn main() {
    // 调用 C 函数(需要 unsafe!)
    unsafe {
        let result = c_add(, );
        println!("5 + 3 = {}", result);  // 8
        
        let c_str = b"Hello, C!\0";  // 注意:需要\0结尾
        let len = c_string_length(c_str.as_ptr());
        println!("String length: {}", len);  // 10
    }
}

Cargo.toml 配置:

代码语言:javascript
复制
[build-dependencies]
cc = "1.0"
代码语言:javascript
复制
// build.rs
fn main() {
    cc::Build::new()
        .file("math_lib.c")
        .compile("math_lib");
}

2. C 调用 Rust 函数

反过来,C 也可以调用 Rust 函数。这在给 C 程序提供 Rust 实现的高性能函数时很有用。

代码语言:javascript
复制
// lib.rs

// 使用 #[no_mangle] 防止名字修饰
// 使用 extern "C" 指定 C 调用约定

#[no_mangle]
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 {
    a + b
}

#[no_mangle]
pub extern "C" fn rust_process_string(input: *const u8, len: usize) -> usize {
    unsafe {
        // 将 C 字符串转换为 Rust 字符串
        let slice = std::slice::from_raw_parts(input, len);
        let s = std::str::from_utf8(slice).unwrap_or("");
        s.len()
    }
}

编译成动态库:

代码语言:javascript
复制
# Cargo.toml
[lib]
crate-type = ["cdylib"]
name = "rust_lib"
代码语言:javascript
复制
cargo build --release

在 C 中调用:

代码语言:javascript
复制
// main.c
#include <stdio.h>
#include <stdint.h>

// 声明 Rust 函数
extern int32_t rust_add(int32_t a, int32_t b);
extern size_t rust_process_string(const uint8_t* input, size_t len);

int main() {
    int result = rust_add(, );
    printf("Rust says: 10 + 20 = %d\n", result);  // 30
    
    const char* str = "Hello from C!";
    size_t len = rust_process_string((const uint8_t*)str, );
    printf("String length: %zu\n", len);
    
    return ;
}

3. 字符串转换:C 字符串 ↔ Rust 字符串

这是 FFI 中最常见的痛点。C 用 \0 结尾的字节数组,Rust 用 UTF-8 编码的 String/&str。

代码语言:javascript
复制
use std::ffi::{CStr, CString};
use std::os::raw::c_char;

extern "C" {
    fn c_get_message() -> *const c_char;
}

fn main() {
    unsafe {
        // C 字符串 → Rust 字符串
        let c_ptr = c_get_message();
        
        // 方法 1:使用 CStr(不拥有所有权)
        let c_str = CStr::from_ptr(c_ptr);
        let rust_str = c_str.to_str().unwrap();
        println!("Message: {}", rust_str);
        
        // Rust 字符串 → C 字符串
        let rust_string = String::from("Hello from Rust!");
        let c_string = CString::new(rust_string).unwrap();
        let c_ptr = c_string.as_ptr();
        
        // ⚠️ 注意:c_string 不能在这里 drop!
        // 如果 C 函数需要保存指针,需要手动管理内存
    }
}

完整示例:安全的字符串 FFI

代码语言:javascript
复制
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::ptr;

/// 将 Rust 字符串转换为 C 字符串
/// 返回一个裸指针,调用者负责释放
#[no_mangle]
pub extern "C" fn rust_create_string(input: *const c_char) -> *mut c_char {
    unsafe {
        if input.is_null() {
            return ptr::null_mut();
        }
        
        let c_str = CStr::from_ptr(input);
        let rust_str = c_str.to_str().unwrap_or("");
        
        // 添加一些 Rust 风格的处理
        let processed = format!("[Rust processed: {}]", rust_str);
        
        // 转换为 C 字符串并返回指针
        let c_string = CString::new(processed).unwrap();
        c_string.into_raw()  // 释放所有权,返回指针
    }
}

/// 释放由 rust_create_string 创建的字符串
/// ⚠️ 必须由调用者显式调用,否则内存泄漏!
#[no_mangle]
pub extern "C" fn rust_free_string(ptr: *mut c_char) {
    unsafe {
        if !ptr.is_null() {
            let _ = CString::from_raw(ptr);  // 重新获取所有权并 drop
        }
    }
}

4. 结构体互操作

Rust 和 C 的结构体可以互操作,但需要注意内存布局。

代码语言:javascript
复制
// Rust 端
#[repr(C)]// 关键!使用 C 的内存布局
#[derive(Debug, Clone, Copy)]
pub struct Point {
    x: f64,
    y: f64,
}

#[no_mangle]
pub extern "C" fn point_distance(p1: Point, p2: Point) -> f64 {
    let dx = p1.x - p2.x;
    let dy = p1.y - p2.y;
    (dx * dx + dy * dy).sqrt()
}
代码语言:javascript
复制
// C 端
#include <stdio.h>
#include <math.h>

typedef struct {
    double x;
    double y;
} Point;

extern double point_distance(Point p1, Point p2);

int main() {
    Point p1 = {0.0, 0.0};
    Point p2 = {3.0, 4.0};
    
    double dist = point_distance(p1, p2);
    printf("Distance: %.2f\n", dist);  // 5.00
    
    return ;
}

⚠️ 重要: #[repr(C)] 是必须的!否则 Rust 可能优化内存布局,导致 C 端读取错误。

5. 错误处理:Result ↔ errno

Rust 用 Result<T, E>,C 用返回值 + errno。怎么转换?

代码语言:javascript
复制
use std::io::{self, ErrorKind};
use std::os::raw::c_int;

#[no_mangle]
pub extern "C" fn safe_divide(a: i32, b: i32, result: *mut i32) -> c_int {
    if b ==  {
        // 设置 errno(需要 libc crate)
        unsafe {
            *libc::__errno_location() = libc::EDOM;
        }
        return -;  // 错误
    }
    
    unsafe {
        *result = a / b;
    }
    // 成功
}

// 更 Rust 风格的封装
pub fn divide(a: i32, b: i32) -> io::Result<i32> {
    let mut result: i32 = ;
    let ret = unsafe {
        safe_divide(a, b, &mut result)
    };
    
    if ret ==  {
        Ok(result)
    } else {
        Err(io::Error::new(
            ErrorKind::InvalidInput,
            "Division by zero"
        ))
    }
}

🐛 常见坑点

坑点 1:忘记 #[repr(C)]

代码语言:javascript
复制
// ❌ 错误:没有指定内存布局
struct Data {
    a: i32,
    b: u8,
    c: i32,
}

// ✅ 正确:使用 C 兼容布局
#[repr(C)]
struct Data {
    a: i32,
    b: u8,
    c: i32,
}

后果: Rust 可能重新排列字段顺序,C 端读取的数据全乱了。

坑点 2:字符串没有\0结尾

代码语言:javascript
复制
// ❌ 错误:忘记\0
let c_str = b"Hello";  // C 端会读取到垃圾数据

// ✅ 正确:添加\0
let c_str = b"Hello\0";
// 或者用 CString
let c_string = CString::new("Hello").unwrap();

坑点 3:内存泄漏

代码语言:javascript
复制
#[no_mangle]
pub extern "C" fn create_string() -> *mut u8 {
    let s = String::from("Hello");
    s.as_ptr() as *mut u8// ❌ 错误:s 会被 drop,指针失效!
}

// ✅ 正确:转移所有权
#[no_mangle]
pub extern "C" fn create_string() -> *mut u8 {
    let s = String::from("Hello");
    CString::new(s).unwrap().into_raw() as *mut u8
}

// 并且提供释放函数
#[no_mangle]
pub extern "C" fn free_string(ptr: *mut u8) {
    unsafe {
        if !ptr.is_null() {
            let _ = CString::from_raw(ptr as *mut c_char);
        }
    }
}

坑点 4:线程安全问题

代码语言:javascript
复制
static mut COUNTER: i32 = ;

#[no_mangle]
pub extern "C" fn increment() {
    unsafe {
        COUNTER += ;  // ❌ 数据竞争!
    }
}

解决: 使用 AtomicI32Mutex

🎯 实战案例:封装 SQLite C 库

让我们看一个真实的 FFI 封装案例——封装 SQLite 的 C API:

代码语言:javascript
复制
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int};
use std::ptr;

// 声明 SQLite 的 C 函数
#[link(name = "sqlite3")]
extern "C" {
    fn sqlite3_open(filename: *const c_char, db: *mut *mut Sqlite3Db) -> c_int;
    fn sqlite3_close(db: *mut Sqlite3Db) -> c_int;
    fn sqlite3_errmsg(db: *mut Sqlite3Db) -> *const c_char;
}

#[repr(C)]
struct Sqlite3Db {
    // 不透明指针,我们不需要知道内部结构
    _private: [u8; ],
}

// 安全的 Rust 封装
pub struct Database {
    db: *mut Sqlite3Db,
}

impl Database {
    pub fn open(path: &str) -> Result<Self, String> {
        let c_path = CString::new(path).unwrap();
        let mut db: *mut Sqlite3Db = ptr::null_mut();
        
        unsafe {
            let result = sqlite3_open(c_path.as_ptr(), &mut db);
            if result !=  {
                let errmsg = sqlite3_errmsg(db);
                let msg = CStr::from_ptr(errmsg).to_string_lossy().into_owned();
                return Err(msg);
            }
        }
        
        Ok(Database { db })
    }
}

impl Drop for Database {
    fn drop(&mut self) {
        unsafe {
            if !self.db.is_null() {
                sqlite3_close(self.db);
            }
        }
    }
}

// 实现 Send 和 Sync(SQLite 连接在特定条件下是线程安全的)
unsafe impl Send for Database {}
unsafe impl Sync for Database {}

fn main() {
    let db = Database::open("test.db").unwrap();
    println!("Database opened successfully!");
    // db 会自动关闭(Drop trait)
}

这个例子展示了:

  • 使用 #[link] 链接外部库
  • 不透明指针(opaque pointer)隐藏 C 结构体细节
  • 安全的 Rust 封装(RAII 模式)
  • 正确的资源清理(Drop trait)
  • 实现 Send/Sync(谨慎!)

🧠 思维导图

36-FFI(外部函数接口)
36-FFI(外部函数接口)

📝 小结

  1. FFI 让 Rust 和 C 可以互操作,但需要处理类型、内存布局、字符串等差异
  2. Rust 调用 C:extern "C" 声明,在 unsafe 块中调用
  3. C 调用 Rust:#[no_mangle]extern "C" 导出函数
  4. 字符串转换: CStr(C→Rust)、CString(Rust→C),注意\0结尾
  5. 结构体互操作: 必须加 #[repr(C)] 保证内存布局兼容
  6. 内存管理: 谁分配谁释放,或用 RAII 封装
  7. 安全封装: 将 unsafe 代码封装在安全的 Rust 接口内

金句时间:

FFI 就像两个国家的边境检查——需要翻译、需要手续,但只要手续齐全,通行无阻。

🔗 下篇预告

FFI 学完了,你是不是觉得:"手动写这些绑定太麻烦了!"

确实,如果一个 C 库有几百个函数,难道要一个个手写?

下篇咱们就来聊聊 宏编程,看看怎么用元编程自动生成代码。届时你会看到:

  • declarative!宏(macro_rules!)
  • procedural macros(过程宏)
  • 怎么用宏减少重复代码

敬请期待 第 37 篇:宏编程

🔗 参考资料

  • The Rustonomicon - FFI
  • Rust by Example - FFI
  • cbindgen - 生成 C 头文件
  • bindgen - 从 C 头文件生成 Rust 绑定
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-06-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Larry的Hub 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • FFI
    • 🎬 引入
    • 📌 核心概念:FFI 是什么?
      • FFI 的定义
      • 为什么需要 FFI?
      • Rust 和 C 的主要差异
    • 💻 代码示例
      • 1. Rust 调用 C 函数
      • 2. C 调用 Rust 函数
      • 3. 字符串转换:C 字符串 ↔ Rust 字符串
      • 4. 结构体互操作
      • 5. 错误处理:Result ↔ errno
    • 🐛 常见坑点
      • 坑点 1:忘记 #[repr(C)]
      • 坑点 2:字符串没有\0结尾
      • 坑点 3:内存泄漏
      • 坑点 4:线程安全问题
    • 🎯 实战案例:封装 SQLite C 库
    • 🧠 思维导图
    • 📝 小结
    • 🔗 下篇预告
    • 🔗 参考资料
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档