
当 Rust 遇上 C:一场跨语言的"相亲",安全与自由的碰撞
想象一下这个场景:
你是一个 Rust 程序员,代码写得风生水起,内存安全、并发无忧。突然有一天,老板走过来:
"咱们有个老的 C 库,性能特别好,你能不能把它集成到咱们的 Rust 项目里?"
你心里一紧:"Rust 和 C……这俩能玩到一起吗?"
答案是:能!而且玩得还不错!
这就是咱们今天要聊的 FFI(Foreign Function Interface,外部函数接口)。简单说,就是让 Rust 和 C(以及其他语言)能够互相调用对方的代码。
但先别急着高兴。Rust 和 C 就像两个性格迥异的人:
让它们合作,就像让一个强迫症和一个随性的人一起做饭——需要一些规则和技巧。
今天,咱们就来学习怎么让这两位"握手言和"。
FFI(Foreign Function Interface)允许一种语言调用另一种语言的函数。在 Rust 中,主要指 Rust 调用 C 或 C 调用 Rust。
特性 | Rust | C |
|---|---|---|
内存安全 | 编译时保证 | 程序员负责 |
空指针 | Option | NULL 指针 |
字符串 | UTF-8,有长度 | 以\0结尾的字节数组 |
错误处理 | Result<T, E> | 返回值/errno |
命名约定 | snake_case | 各种风格都有 |
关键问题: 这些差异需要在 FFI 边界处进行转换。
这是最常见的场景。假设我们有一个 C 库:
// 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;
}
编译成动态库:
# Linux/macOS
gcc -shared -fPIC -o libmath_lib.so math_lib.c
# Windows
gcc -shared -o math_lib.dll math_lib.c
在 Rust 中调用:
// 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 配置:
[build-dependencies]
cc = "1.0"
// build.rs
fn main() {
cc::Build::new()
.file("math_lib.c")
.compile("math_lib");
}
反过来,C 也可以调用 Rust 函数。这在给 C 程序提供 Rust 实现的高性能函数时很有用。
// 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()
}
}
编译成动态库:
# Cargo.toml
[lib]
crate-type = ["cdylib"]
name = "rust_lib"
cargo build --release
在 C 中调用:
// 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 ;
}
这是 FFI 中最常见的痛点。C 用 \0 结尾的字节数组,Rust 用 UTF-8 编码的 String/&str。
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
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
}
}
}
Rust 和 C 的结构体可以互操作,但需要注意内存布局。
// 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()
}
// 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 端读取错误。
Rust 用 Result<T, E>,C 用返回值 + errno。怎么转换?
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"
))
}
}
// ❌ 错误:没有指定内存布局
struct Data {
a: i32,
b: u8,
c: i32,
}
// ✅ 正确:使用 C 兼容布局
#[repr(C)]
struct Data {
a: i32,
b: u8,
c: i32,
}
后果: Rust 可能重新排列字段顺序,C 端读取的数据全乱了。
// ❌ 错误:忘记\0
let c_str = b"Hello"; // C 端会读取到垃圾数据
// ✅ 正确:添加\0
let c_str = b"Hello\0";
// 或者用 CString
let c_string = CString::new("Hello").unwrap();
#[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);
}
}
}
static mut COUNTER: i32 = ;
#[no_mangle]
pub extern "C" fn increment() {
unsafe {
COUNTER += ; // ❌ 数据竞争!
}
}
解决: 使用 AtomicI32 或 Mutex。
让我们看一个真实的 FFI 封装案例——封装 SQLite 的 C API:
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] 链接外部库
extern "C" 声明,在 unsafe 块中调用#[no_mangle] 和 extern "C" 导出函数#[repr(C)] 保证内存布局兼容金句时间:
FFI 就像两个国家的边境检查——需要翻译、需要手续,但只要手续齐全,通行无阻。
FFI 学完了,你是不是觉得:"手动写这些绑定太麻烦了!"
确实,如果一个 C 库有几百个函数,难道要一个个手写?
下篇咱们就来聊聊 宏编程,看看怎么用元编程自动生成代码。届时你会看到:
敬请期待 第 37 篇:宏编程!