如何定义 #[repr(C)] 结构体?
解读
国内校招/社招面试里,这道题常被用来**快速区分“写过 FFI”与“只写过纯 Rust”**的候选人。
面试官真正想听的不是“加一行属性”那么简单,而是:
- 你为什么敢把内存布局交给 C 决定;
- 你怎么保证 Rust 侧与 C 侧看到的字节级布局完全一致;
- 如果布局不对,你会用哪些工具在第一时间定位。
答不到这三层,基本会被归为“只看过书”。
知识点
- repr(C) 的语义:把 Rust struct 的内存布局按 C 语言规则排列,字段顺序、对齐、填充均由“目标平台 C ABI”决定,Rust 编译器不再重排。
- 对齐与填充规则:每个字段取其
align_of::<T>()的倍数开始;结构体整体大小是其最大对齐值的倍数。 - 字段顺序敏感:Rust 默认可重排,C 不允许;
repr(C)会冻结顺序,因此字段声明顺序必须与头文件一致。 - ZST 与 1-ZST:Rust 的零大小类型在
repr(C)下仍占 0 字节,但 C 侧若声明为char _dummy[0]会触发 UB;必须补一字节占位。 - enum 的陷阱:
repr(C)只能用于无变体数据的 C-like enum,且默认底层标签是平台 int;若 C 侧用uint8_t,必须再写repr(u8)。 - UB 边界:
– 不可包含String、Vec、Box等带有 Rust 特有分配器指针的字段;
– 不可包含bool除非 C 侧明确用_Bool且字节值严格 0/1;
– 不可包含char(4 字节)去对 C 的char(1 字节)。 - 验证工具链:
–cargo expand看展开后属性;
–#[repr(C)]+static_assertions::assert_eq_size!做编译期大小断言;
–bindgen --rust-target nightly自动生成 Rust 侧绑定,反向比对布局;
–cbindgen从 Rust 生成头文件,与已有 .h 文件 diff。 - 多平台差异:32/64 位、Windows MSVC/GNU、ARM packed 对齐差异;CI 里必须交叉编译跑测试。
答案
use std::os::raw::{c_int, c_char};
/// 与 C 头文件里
/// typedef struct {
/// int id;
/// char name[32];
/// } person_t;
/// 保持一字节不差。
#[repr(C)] // 1. 强制 C 布局
#[derive(Debug, Copy, Clone)] // 2. 建议加派生,方便调试
pub struct Person {
pub id: c_int, // 3. 类型用 libc 别名,避免平台差异
pub name: [c_char; 32], // 4. 定长数组,拒绝 String
}
/// 编译期断言:Rust 侧大小 == C 侧大小
#[cfg(test)]
mod tests {
use super::*;
use std::mem::size_of;
#[test]
fn layout_sanity() {
assert_eq!(size_of::<Person>(), 36); // 4 + 32
assert_eq!(size_of::<Person>(), size_of::<ffi::person_t>());
}
}
关键点逐条说明:
#[repr(C)]必须写在 struct 前面,属性作用域仅对该项生效;- 所有字段类型必须能在 C 侧找到一字节兼容的对应类型;
- 数组长度、字段顺序、对齐值必须与头文件完全一致;
- 用
std::mem::transmute或指针强转前,先用 static assertion 保证 size/align 相等,否则立即 UB; - 若 C 侧用
#pragma pack(1),Rust 侧必须再补#[repr(C, packed(1))],否则会出现“Rust 侧 36 字节、C 侧 33 字节”的惨案。
拓展思考
- “双 repr” 组合:
#[repr(C, packed(1)]与#[repr(C, align(64))]同时出现时的优先级与交互规则是什么? - Rust 1.82 即将稳定的
repr(simd)与repr(C)混用,如何保证向量类型在 FFI 边界不崩? - async FFI:如果结构体里要传一个
tokio::task::JoinHandle,你打算**怎样在repr(C)结构体里表达“不透明指针”**才能既安全又避免泄漏? - CI 实战:在 GitHub Actions 里,如何一次性跑
bindgen + cbindgen + cross验证 x86_64、aarch64、riscv32 三种目标的 layout 一致性? - 面试反杀:当面试官追问“为什么不用
repr(packed)直接省内存”时,你可以用 “未对齐访问在 ARM 上会触发 SIGBUS” 把话题拉进底层硬件,反向展示你懂 CPU 架构,从而拿到加分。