《Rust 编译器原理》完整目录
- 前言
- 第1章 编译管线全景:从源码到机器码的完整旅程
- 第2章 所有权系统:编译期内存管理的核心机制
- 第3章 借用检查器:编译器如何证明内存安全
- 第4章 生命周期:编译器如何推断引用的有效范围
- 第5章 内存布局:编译器如何排列数据
- 第6章 单态化:泛型的编译期展开
- 第7章 Trait 静态分发:零成本抽象的编译器实现
- 第8章 Trait Object 与虚表:运行时多态的内存布局
- 第9章 async/await:状态机的编译器变换
- 第10章 Pin、Waker 与 Future:异步运行时的三大支柱
- 第11章 闭包:匿名函数的编译器实现
- 第12章 unsafe:安全抽象的逃生舱
- 第13章 FFI:与 C 世界的桥梁(当前)
- 第14章 宏系统:编译期的元编程引擎
- 第15章 MIR 优化:编译器的中间表示与优化管线
- 第16章 LLVM 代码生成:从 MIR 到机器码
- 第17章 增量编译:让重编译只做必要的事
- 第18章 设计哲学与架构决策
第13章 FFI:与 C 世界的桥梁
"ABI 是两种语言之间的握手协议------参数怎么传、返回值放哪里、谁来清理栈、谁来释放内存。" ------ 系统编程格言
:::tip 本章要点
extern "C"告诉编译器使用 C ABI 调用约定:参数通过特定寄存器和栈传递,遵循平台标准repr(C)保证 struct 的内存布局与 C 编译器完全一致------字段顺序、对齐、padding 都可预测- Rust 的
String/&str是 (ptr, len) 胖指针,C 的字符串是 null 结尾的char*------需要CStr/CString转换 #[no_mangle]阻止编译器对函数名进行 name mangling,使 C 代码可以通过原始名称链接- 回调函数通过函数指针 + void* 上下文的模式在 Rust 与 C 之间传递
- FFI 边界的内存管理遵循谁分配谁释放原则------不要混用分配器
- 安全封装(Safe Wrapper Pattern)是将 unsafe FFI 调用包装为安全 Rust API 的标准做法 :::
13.1 为什么需要 FFI
Rust 诞生在一个以 C 为基础的世界中。操作系统内核用 C 写成,数十年积累的高质量库------从 SQLite 到 OpenSSL------都以 C API 的形式存在。FFI(Foreign Function Interface)解决三个核心需求:
调用 C 库。 当你需要使用 SQLite、OpenSSL 或任何已有的 C 库时,FFI 让 Rust 直接调用这些函数,无需重写。
调用系统 API。 POSIX API、Windows API 几乎全部以 C ABI 暴露。Rust 标准库在 library/std/src/sys/ 目录下大量使用 FFI 调用底层系统函数------文件系统、网络 I/O、线程、随机数生成,全部通过 extern "C" 块实现。
将 Rust 嵌入其他语言。 Python、C/C++、Go、Ruby 都可以通过 C ABI 调用 Rust 编写的共享库。
C ABI 是系统编程世界的通用语言。 几乎所有语言都能生成或调用符合 C ABI 的代码,Rust 选择以此作为与外部世界交互的桥梁。
13.2 extern "C" 与 ABI 规范:编译器做了什么
ABI(Application Binary Interface)定义了二进制层面的交互规则:参数放哪些寄存器、返回值怎么传、谁清理栈、哪些寄存器需要保存。
rust
// Rust ABI------编译器自由优化,不保证跨版本兼容
fn rust_abi(a: i32, b: i64) -> i32 { a + b as i32 }
// C ABI------遵循平台标准,可以被 C/Python/Go 调用
extern "C" fn c_abi(a: i32, b: i64) -> i32 { a + b as i32 }
在 rustc_target/src/spec/abi_map.rs 中,AbiMap 负责将源码中的 ABI 标注映射为规范化的 CanonAbi:
rust
// 来自 rustc_target/src/spec/abi_map.rs(简化)
pub fn canonize_abi(&self, extern_abi: ExternAbi, has_c_varargs: bool) -> AbiMapping {
match (extern_abi, arch) {
(ExternAbi::C { .. }, _) => CanonAbi::C,
(ExternAbi::Rust | ExternAbi::RustCall, _) => CanonAbi::Rust,
(ExternAbi::System { .. }, ArchKind::X86)
if os == OsKind::Windows && !has_c_varargs =>
{
CanonAbi::X86(X86Call::Stdcall) // Windows x86 上 System = Stdcall
}
(ExternAbi::System { .. }, _) => CanonAbi::C, // 其他平台 System = C
// ...
}
}
rustc_target/src/callconv/mod.rs 中的 FnAbi 结构描述函数在特定 ABI 下的完整调用信息,每个参数的传递方式由 PassMode 枚举描述:
rust
// 来自 rustc_target/src/callconv/mod.rs
pub enum PassMode {
Ignore, // ZST,忽略
Direct(ArgAttributes), // 直接通过寄存器
Pair(ArgAttributes, ArgAttributes), // ScalarPair,两个寄存器
Cast { pad_i32: bool, cast: Box<CastTarget> }, // 类型转换后传递
Indirect { attrs: ArgAttributes, meta_attrs: Option<ArgAttributes>, on_stack: bool },
}
在 x86-64 上,classify_arg 按 System V ABI 规则将参数分为 Int(通用寄存器 rdi, rsi, rdx, rcx, r8, r9)和 Sse(SSE 寄存器 xmm0-xmm7)两类。超出寄存器容量的参数通过栈传递。
13.3 类型映射:Rust 类型与 C 类型
core/src/ffi/primitives.rs 中定义了与 C 类型精确对应的类型别名:
| C 类型 | Rust FFI 类型 | 说明 |
|---|---|---|
char |
c_char |
平台相关:ARM Linux 为 u8,x86 Linux 为 i8 |
int / unsigned int |
c_int / c_uint |
通常 i32 / u32 |
long |
c_long |
Linux 64 位: i64, Windows: i32 |
double |
c_double |
通常 f64 |
void* |
*mut c_void |
通用指针 |
size_t |
usize |
指针大小的无符号整数 |
c_char 的符号性差异值得注意------编译器源码中有大段注释引用各平台的 ABI 文档。Apple 平台即使在 ARM 上也强制 signed char,而 ARM Linux 默认 unsigned char。
字符串转换:CStr 与 CString
Rust 标准库 library/std/src/ffi/mod.rs 中详细说明了两种字符串体系的差异:Rust 字符串保证 UTF-8、存储长度、可含内部 \0;C 字符串无编码保证、以 \0 结尾、不可含内部 \0。
rust
use std::ffi::{CStr, CString, c_char};
fn rust_to_c() {
let c_string = CString::new("Hello, C!").expect("包含内部 \\0");
unsafe { puts(c_string.as_ptr()); }
}
unsafe fn c_to_rust(c_buf: *const c_char) {
let c_str = CStr::from_ptr(c_buf);
// to_str() 做 UTF-8 验证;to_string_lossy() 做有损转换
match c_str.to_str() {
Ok(s) => println!("{}", s),
Err(_) => println!("{}", c_str.to_string_lossy()),
}
}
// Rust 1.77+ 的 C 字符串字面量------编译期保证末尾 \0 和无内部 \0
let greeting: &CStr = c"Hello, C world!";
CString::new 返回 Result 是因为内部 \0 会导致 C 截断字符串------这是编译器无法静态检查的运行时约束。
13.4 repr(C):确保 C 兼容的内存布局
repr(C) 强制使用 C 的布局规则:字段按声明顺序排列,对齐遵循 C ABI 标准。
rust
#[repr(C)]
struct Misaligned {
a: u8, // 偏移 0
// 3 字节 padding
b: u32, // 偏移 4
c: u8, // 偏移 8
// 3 字节 padding
}
// 总大小 12 字节(6 字节 padding)
#[repr(C)]
enum Color { Red = 0, Green = 1, Blue = 2 }
// 等价于 C 的 enum,通常 4 字节
#[repr(C, u8)]
enum SmallColor { Red = 0, Green = 1, Blue = 2 }
// 指定底层类型为 u8,大小 1 字节
注意:repr(C) 不允许编译器重排字段------如果你需要减少 padding,必须手动调整字段顺序(将大对齐字段放前面)。
repr(C) 联合体
FFI 中经常需要联合体来表示 C 的 union:
rust
#[repr(C)]
union Value {
i: i32,
f: f32,
b: bool,
}
// 等价于 C 的 union Value { int i; float f; bool b; };
// 大小 = max(4, 4, 1) = 4 字节
// 访问联合体字段是 unsafe 的------编译器无法追踪当前活跃的字段
let v = Value { i: 42 };
let i = unsafe { v.i }; // OK,因为我们知道是 i 被设置的
13.5 从 Rust 调用 C:extern 块、链接与 bindgen
rust
use std::ffi::{c_int, c_double, c_void};
extern "C" {
fn abs(x: c_int) -> c_int;
fn sqrt(x: c_double) -> c_double;
fn malloc(size: usize) -> *mut c_void;
fn free(ptr: *mut c_void);
}
fn main() {
unsafe {
assert_eq!(abs(-42), 42);
println!("sqrt(2) = {}", sqrt(2.0));
}
}
通过 build.rs 控制链接:
rust
// build.rs
fn main() {
println!("cargo:rustc-link-lib=static=foo"); // 静态链接 libfoo.a
println!("cargo:rustc-link-search=native=/path");
println!("cargo:rustc-link-lib=dylib=bar"); // 动态链接 libbar.so
}
bindgen 从 C 头文件自动生成 Rust 绑定,在 build.rs 中集成后每次构建自动更新:
rust
// build.rs
fn main() {
let bindings = bindgen::Builder::default()
.header("wrapper.h")
.generate().expect("生成绑定失败");
let out = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
bindings.write_to_file(out.join("bindings.rs")).unwrap();
}
13.6 从 C 调用 Rust:#[no_mangle]、extern "C" fn 与 cdylib
rust
#[no_mangle]
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 { a + b }
编译器源码 rustc_symbol_mangling/src/lib.rs 中记录了 #[no_mangle] 的处理逻辑:
rust
// 来自 rustc_symbol_mangling/src/lib.rs(简化)
fn compute_symbol_name(tcx, instance, ...) -> String {
let attrs = tcx.codegen_instance_attrs(instance.def);
if let Some(name) = attrs.symbol_name { return name.to_string(); }
if attrs.flags.contains(CodegenFnAttrFlags::NO_MANGLE) {
return tcx.item_name(def_id).to_string(); // 保留原始名称
}
// 否则使用 v0 mangling scheme 编码 crate 路径、泛型参数等
}
没有 #[no_mangle],rust_add 会变成类似 _RNvCskwGfYPst2Cb_7my_crate8rust_add 的符号名------C 链接器找不到。
要构建 C 可用的库,在 Cargo.toml 中指定:
toml
[lib]
crate-type = ["cdylib"] # .so (Linux) / .dylib (macOS) / .dll (Windows)
# 或者
crate-type = ["staticlib"] # .a (Unix) / .lib (Windows)
编译后,C 代码通过头文件声明函数签名并链接:
c
// 声明 Rust 函数
extern int rust_add(int a, int b);
int main() { return rust_add(3, 4); } // 链接: gcc main.c -L target/release -lmy_lib
13.7 回调函数:将 Rust 闭包传递给 C
C 库常接受函数指针回调------排序的比较器、事件循环的处理器、遍历操作的访问器。好的 C API 遵循函数指针 + void* 上下文 的模式,void* 让调用者传递任意数据给回调函数,避免全局变量。
无状态回调
最简单的情况------回调不需要访问任何外部状态:
rust
type CCallback = extern "C" fn(event: i32, user_data: *mut std::ffi::c_void);
extern "C" { fn register_callback(cb: CCallback, user_data: *mut std::ffi::c_void); }
extern "C" fn simple_callback(event: i32, _data: *mut std::ffi::c_void) {
println!("收到事件: {}", event);
}
fn register_simple() {
unsafe { register_callback(simple_callback, std::ptr::null_mut()); }
}
有状态回调:通过 void* 传递上下文
rust
struct Context { name: String, count: u32 }
extern "C" fn stateful_callback(event: i32, user_data: *mut std::ffi::c_void) {
let ctx = unsafe { &mut *(user_data as *mut Context) };
ctx.count += 1;
println!("[{}] 事件 {} (第 {} 次)", ctx.name, event, ctx.count);
}
fn register_stateful() {
let ctx = Box::new(Context { name: "监控".into(), count: 0 });
let raw = Box::into_raw(ctx); // 转移所有权,防止被 drop
unsafe { register_callback(stateful_callback, raw as *mut std::ffi::c_void); }
// 重要:稍后需要用 Box::from_raw(raw) 回收内存!
}
传递闭包:trampoline 模式
Rust 闭包捕获了环境变量,不是裸函数指针。通过泛型 trampoline 函数实现转换:
rust
extern "C" fn closure_trampoline<F: FnMut(i32)>(event: i32, data: *mut std::ffi::c_void) {
let closure = unsafe { &mut *(data as *mut F) };
closure(event);
}
fn register_closure<F: FnMut(i32) + 'static>(closure: F) {
let boxed = Box::new(closure);
let raw = Box::into_raw(boxed);
unsafe {
register_callback(closure_trampoline::<F>, raw as *mut std::ffi::c_void);
}
}
// 使用
let mut counter = 0;
register_closure(move |event| {
counter += 1;
println!("闭包: 事件 {}, 计数 {}", event, counter);
});
trampoline 函数是泛型的,Rust 为每种闭包类型生成特化版本------利用单态化保证类型安全。关键注意点:通过 void* 传递的上下文对象必须在回调期间保持存活 ,通常用 Box::into_raw 延长生命周期。
13.8 跨 FFI 边界的错误处理
Rust 的 Result 和 C 的错误码需要精心桥接。最关键的原则:panic 不能跨越 FFI 边界。
rust
#[no_mangle]
pub extern "C" fn safe_parse(input: *const std::ffi::c_char, result: *mut i32) -> i32 {
std::panic::catch_unwind(|| {
if input.is_null() || result.is_null() { return -1; }
let s = unsafe { std::ffi::CStr::from_ptr(input) };
match s.to_str().ok().and_then(|s| s.parse::<i32>().ok()) {
Some(n) => { unsafe { *result = n; } 0 }
None => -2,
}
}).unwrap_or(-99) // panic 被捕获,返回错误码
}
推荐的完整模式:定义 #[repr(C)] 错误码枚举,配合线程局部的错误消息缓冲区(类似 errno + strerror):
rust
/// FFI 错误码
#[repr(C)]
pub enum ErrorCode { Success = 0, NullPointer = -1, InvalidUtf8 = -2, InternalError = -3 }
thread_local! {
static LAST_ERROR: std::cell::RefCell<Option<String>> = std::cell::RefCell::new(None);
}
fn set_last_error(msg: String) {
LAST_ERROR.with(|e| *e.borrow_mut() = Some(msg));
}
/// C 端调用获取错误详情
#[no_mangle]
pub extern "C" fn get_last_error(buf: *mut u8, buf_len: i32) -> i32 {
LAST_ERROR.with(|e| {
match e.borrow().as_ref() {
None => 0,
Some(msg) => {
let bytes = msg.as_bytes();
if buf.is_null() || bytes.len() + 1 > buf_len as usize { return -1; }
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf, bytes.len());
*buf.add(bytes.len()) = 0;
}
bytes.len() as i32
}
}
})
}
C 端可以这样使用:先调用业务函数检查返回值,失败时调用 get_last_error 获取详细信息。这个模式在 Rust 生态的 C 绑定库中被广泛采用。
13.9 跨 FFI 的内存管理:谁分配,谁释放
核心原则:谁分配,谁释放。 Rust 和 C 使用不同的分配器,混用是未定义行为。
模式一:Rust 分配,Rust 释放(不透明句柄)
最安全的模式。C 代码持有不透明指针,创建和销毁都由 Rust 完成:
rust
pub struct Database { /* Rust 内部结构 */ }
#[no_mangle]
pub extern "C" fn db_open() -> *mut Database {
Box::into_raw(Box::new(Database { /* ... */ }))
}
#[no_mangle]
pub unsafe extern "C" fn db_close(db: *mut Database) {
if !db.is_null() { let _ = Box::from_raw(db); } // 重建 Box 触发 drop
}
模式二:C 分配,C 释放
使用 C 库返回的内存时,必须用 C 库提供的释放函数:
rust
extern "C" {
fn strdup(s: *const std::ffi::c_char) -> *mut std::ffi::c_char;
fn free(ptr: *mut std::ffi::c_void);
}
unsafe {
let copy = strdup(c"hello".as_ptr());
// copy 是 C 的 malloc 分配的,必须用 free 释放
free(copy as *mut std::ffi::c_void);
}
模式三:调用者分配缓冲区
由调用方分配缓冲区,被调用方填充数据------避免了跨边界的所有权转移:
rust
#[no_mangle]
pub extern "C" fn format_number(val: i64, buf: *mut u8, buf_len: i32) -> i32 {
if buf.is_null() || buf_len <= 0 { return -1; }
let s = format!("{}", val);
if s.len() + 1 > buf_len as usize { return -1; }
unsafe {
std::ptr::copy_nonoverlapping(s.as_ptr(), buf, s.len());
*buf.add(s.len()) = 0;
}
s.len() as i32
}
13.10 安全封装:将 C API 包装为安全的 Rust API
store.set(key, value)?;
// 自动 drop"] end subgraph "安全封装层" S["struct KvStore(NonNull<ffi::kv_store>)
impl Drop: 调用 ffi::kv_close
所有方法返回 Result"] end subgraph "底层 FFI(unsafe)" F["extern "C" { fn kv_open/close/set/get... }"] end U --> S --> F style U fill:#10b981,color:#fff,stroke:none style S fill:#3b82f6,color:#fff,stroke:none style F fill:#f59e0b,color:#fff,stroke:none
安全封装的要素:
rust
mod ffi {
#[repr(C)]
pub struct kv_store { _opaque: [u8; 0] } // 不透明类型
extern "C" {
pub fn kv_open(path: *const std::ffi::c_char) -> *mut kv_store;
pub fn kv_close(store: *mut kv_store);
pub fn kv_set(s: *mut kv_store, k: *const std::ffi::c_char, v: *const std::ffi::c_char) -> i32;
pub fn kv_get(s: *mut kv_store, k: *const std::ffi::c_char) -> *const std::ffi::c_char;
}
}
pub struct KvStore { inner: std::ptr::NonNull<ffi::kv_store> }
impl KvStore {
pub fn open(path: &str) -> Result<Self, KvError> {
let c_path = std::ffi::CString::new(path).map_err(|_| KvError::InvalidPath)?;
let ptr = unsafe { ffi::kv_open(c_path.as_ptr()) };
std::ptr::NonNull::new(ptr).map(|p| KvStore { inner: p }).ok_or(KvError::OpenFailed)
}
pub fn get(&self, key: &str) -> Result<String, KvError> {
let c_key = std::ffi::CString::new(key).map_err(|_| KvError::InvalidKey)?;
let ptr = unsafe { ffi::kv_get(self.inner.as_ptr() as *mut _, c_key.as_ptr()) };
if ptr.is_null() { return Err(KvError::NotFound); }
// 关键:立即复制,因为 C 指针可能在下次调用后失效
let s = unsafe { std::ffi::CStr::from_ptr(ptr) };
s.to_str().map(|s| s.to_string()).map_err(|_| KvError::InvalidUtf8)
}
}
impl Drop for KvStore {
fn drop(&mut self) { unsafe { ffi::kv_close(self.inner.as_ptr()); } }
}
检查清单: (1) 所有 FFI 调用集中在 ffi 模块;(2) RAII 管理 C 资源;(3) NonNull 代替裸指针;(4) 立即复制借用的 C 数据;(5) C 错误码转 Result;(6) 字符串转换处理 \0 和 UTF-8;(7) 考虑线程安全性。
13.11 平台特定代码:cfg(target_os) 与条件编译
FFI 代码天然与平台绑定。Rust 的条件编译系统为不同平台编写不同绑定:
rust
#[cfg(target_os = "linux")]
extern "C" { fn epoll_create1(flags: i32) -> i32; }
#[cfg(target_os = "macos")]
extern "C" { fn kqueue() -> i32; }
#[cfg(target_os = "windows")]
extern "system" { fn CreateIoCompletionPort(/* ... */) -> *mut std::ffi::c_void; }
注意 Windows 使用 extern "system" 而非 extern "C"------编译器会将其映射为 Stdcall(x86)或 C ABI(x86-64)。
Rust 标准库在 library/std/src/sys/pal/ 下按平台分离了所有 FFI 实现,通过 PAL(Platform Abstraction Layer)对上层暴露统一接口:
bash
library/std/src/sys/
├── pal/ # 平台抽象层
│ ├── unix/ # Unix (Linux, macOS, BSDs...)
│ ├── windows/ # Windows
│ └── ...
├── fs/ # 文件系统(内部按平台分发)
├── net/ # 网络
├── thread/ # 线程
├── sync/ # 同步原语
└── random/ # 随机数
这种架构模式值得学习:底层按平台分离 FFI 绑定,上层通过统一 trait 抽象差异。 用户代码只面对统一的 Rust API,无需关心底层是 epoll、kqueue 还是 IOCP。
常用的 cfg 条件:
| cfg 条件 | 说明 | 示例值 |
|---|---|---|
target_os |
操作系统 | "linux", "macos", "windows" |
target_arch |
CPU 架构 | "x86_64", "aarch64" |
target_family |
平台家族 | "unix", "windows" |
target_env |
工具链环境 | "gnu", "musl", "msvc" |
target_pointer_width |
指针宽度 | "32", "64" |
13.12 实战示例:封装 C 加密库
综合运用前面所有知识点,假设封装一个 C 加密库:
rust
// ffi 层
mod ffi {
#[repr(C)]
pub struct crypto_ctx { _opaque: [u8; 0], _marker: std::marker::PhantomData<*mut ()> }
extern "C" {
pub fn crypto_new() -> *mut crypto_ctx;
pub fn crypto_free(ctx: *mut crypto_ctx);
pub fn crypto_set_key(ctx: *mut crypto_ctx, key: *const u8, len: usize) -> i32;
pub fn crypto_encrypt(ctx: *mut crypto_ctx, input: *const u8, in_len: usize,
out: *mut u8, out_len: *mut usize) -> i32;
pub fn crypto_error_msg(code: i32) -> *const std::ffi::c_char;
}
}
// 安全封装层
pub struct CryptoContext { inner: std::ptr::NonNull<ffi::crypto_ctx> }
impl CryptoContext {
pub fn new() -> Result<Self, CryptoError> {
let ptr = unsafe { ffi::crypto_new() };
std::ptr::NonNull::new(ptr).map(|p| Self { inner: p }).ok_or(CryptoError::InitFailed)
}
pub fn set_key(&mut self, key: &[u8]) -> Result<(), CryptoError> {
let code = unsafe { ffi::crypto_set_key(self.inner.as_ptr(), key.as_ptr(), key.len()) };
if code == 0 { Ok(()) } else { Err(CryptoError::from_code(code)) }
}
pub fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>, CryptoError> {
let mut out = vec![0u8; plaintext.len() + 256];
let mut out_len = out.len();
let code = unsafe {
ffi::crypto_encrypt(self.inner.as_ptr() as *mut _, plaintext.as_ptr(),
plaintext.len(), out.as_mut_ptr(), &mut out_len)
};
if code != 0 { return Err(CryptoError::from_code(code)); }
out.truncate(out_len);
Ok(out)
}
}
impl Drop for CryptoContext {
fn drop(&mut self) { unsafe { ffi::crypto_free(self.inner.as_ptr()); } }
}
// 用户代码------零 unsafe
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut ctx = CryptoContext::new()?;
ctx.set_key(b"my-32-byte-secret-key-here!!!!!!")?;
let encrypted = ctx.encrypt(b"Hello, secure world!")?;
println!("密文 {} 字节", encrypted.len());
Ok(())
}
13.13 cbindgen:从 Rust 代码生成 C 头文件
bindgen 将 C 头文件转为 Rust 绑定,cbindgen 做相反的事------从 Rust 代码生成 C/C++ 头文件。
bash
cargo install cbindgen
cbindgen --lang c --output include/my_lib.h
给定以下 Rust 代码:
rust
#[repr(C)]
pub struct Point { pub x: f64, pub y: f64 }
#[no_mangle]
pub extern "C" fn point_distance(a: *const Point, b: *const Point) -> f64 {
if a.is_null() || b.is_null() { return -1.0; }
let (a, b) = unsafe { (&*a, &*b) };
((a.x - b.x).powi(2) + (a.y - b.y).powi(2)).sqrt()
}
cbindgen 生成:
c
#ifndef MY_LIB_H
#define MY_LIB_H
#include <stdint.h>
typedef struct Point { double x; double y; } Point;
double point_distance(const Point *a, const Point *b);
#endif
在 build.rs 中集成后,每次 cargo build 时头文件自动更新:
rust
// build.rs
fn main() {
cbindgen::Builder::new()
.with_crate(std::env::var("CARGO_MANIFEST_DIR").unwrap())
.with_language(cbindgen::Language::C)
.generate().expect("cbindgen failed")
.write_to_file("include/my_lib.h");
}
配置文件 cbindgen.toml 可以控制命名风格、包含守卫、自动生成警告等细节,保证 Rust 与 C 接口始终同步。
13.14 FFI 安全性总结
FFI 边界是 Rust 安全保证的断裂带。以下不变量需要手动保证:
| 不变量 | 常见错误 |
|---|---|
| 类型大小和对齐匹配 | 用 i32 对应 C 的 long(64 位 Linux 上是 8 字节) |
| 内存所有权清晰 | 用 Rust 的 drop 释放 C malloc 的内存 |
| 字符串正确转换 | 把 &str 直接转为 *const c_char(缺少 \0) |
| 空指针检查 | 未检查 C 返回值是否为 NULL |
| panic 不跨越 FFI | 在 extern "C" fn 中未捕获 panic |
| 指针有效性 | 使用已被 C 释放的指针 |
bool/enum 有效值 |
C 传递了无效的 enum 判别值 |
掌握了 FFI 的全部机制,我们看到编译器在语言边界处的退让------从完全验证退化为"信任程序员"。unsafe 块是编译器对程序员说:"我检查不了了,你来保证正确性。" 安全封装层的意义在于将这种信任压缩到尽可能小的代码范围内。
下一章我们将回到编译器大展身手的领域:宏展开。声明宏和过程宏在编译流水线的哪个阶段被处理?token tree 到底是什么?编译器如何保证宏生成的代码不会意外地与使用者的代码发生命名冲突?