本节摘要:FFI 让 Rust 与 C 互相调用——extern 声明外来函数、CString 打磨字符串边界;no_std 则砍掉标准库,把 Rust 带进裸机与嵌入式。两条通道都建立在上一节的越权框架上。读完你能读懂并写出简单的跨语言接口,理解 Rust 进军嵌入式与内核的路径。
use std::os::raw::c_int; #[link(name = "archive_sys")] extern "C" { fn sys_checksum(data: *const u8, len: u32) -> c_int; // 外来函数 } fn checksum(bytes: &[u8]) -> i32 { let r = unsafe { sys_checksum(bytes.as_ptr(), bytes.len() as u32) // 调用必须越权 }; r as i32 }
extern "C" 块声明"这些符号住在别处",调用自动成为 unsafe——C 侧不守借用铁律,法官无从审它。边界的脏活在两侧完成:Rust 字符串传给 C 要先转 CString(保证结尾零字节),C 返回的指针要么立刻复制、要么包进安全类型。方向反过来(C 调 Rust)用 #[no_mangle] pub extern "C" fn 导出——很多项目用 Rust 重写 C 库的某个热点模块就是这条通道。
#![no_std] // 放弃标准库,只留核心库 core fn add(a: i32, b: i32) -> i32 { a + b }
标准库带着堆分配、线程、网络;嵌入式与内核模块没有这些。no_std 项目只依赖 core(那些不需要操作系统的部分:迭代器、Option、Trait 全在),堆要自己接分配器。第 3 章的所有权法条在裸机上原封不动——这正是 Rust 能进嵌入式与操作系统领域的资格证明。
use std::ffi::{c_char, c_int, CString}; #[repr(C)] // 布局按 C 判例集,跨界的先决条件 struct Report { severity: c_int, label: *const c_char, // C 字符串指针,非 Rust 的 &str } extern "C" { fn ht_report(r: *const Report) -> c_int; // 声明外部符号,不定义 } fn main() { let c_label = CString::new("越权访问").expect("无内部 NUL"); let r = Report { severity: 2, label: c_label.as_ptr() }; let rc = unsafe { ht_report(&r) }; // 跨界调用必须 unsafe:契约由人担保 println!("外部返回 {}", rc); } // c_label 析构,指针随之作废
三个边界要点全在代码注释里:repr(C) 固定布局、CString 处理 NUL 结尾差异、指针生命周期系于 Rust 侧属主。反向出境(Rust 函数给 C 调)加 #[no_mangle] pub extern "C" fn,panic 不许穿越边界,需要 catch_unwind 拦截——第 6 章的伏笔在此兑现。
#![no_std] // 放弃分配器与线程等默认设施 mod mini { pub fn crc8(data: &[u8]) -> u8 { let mut crc: u8 = 0; for &b in data { crc ^= b; for _ in 0..8 { let msb = crc & 0x80 != 0; crc <<= 1; if msb { crc ^= 0x07; } } } crc } } fn main() { // no_std 下 main 需自配入口,此为示意 let sum = mini::crc8(b"HT"); let _ = sum; }
no_std 裁掉的是堆分配、线程、网络这些"需要运行时"的设施,核心语言与部分无分配库(core)完整保留。堆需求可用外部分配器(嵌入式接 alloc crate)或定长容器满足。判据很简单:目标平台有没有操作系统级别的服务——没有就是 no_std 辖区。
| 事项 | 纪律 | 违反后果 |
|---|---|---|
| 内存布局 | 结构体加 repr(C) | 字段错位、读取脏数据 |
| 字符串 | CString / CStr 转换 | NUL 截断或越界读 |
| panic | 边界处 catch_unwind | 未定义行为 |
| 属主 | 明确哪侧负责释放 | 泄漏或双重释放 |
| 空指针 | 入口处判空 | 空解引用崩盘 |
FFI 是第 8 章 Send/Sync 与本章 unsafe 的联合考点:外部资源几乎都是非 Send 的裸指针,包装类型要么 unsafe impl Send 并写明担保,要么明确不 Send 阻止其跨线程。
FFI 边界两侧都要测试:Rust 侧用常规测试覆盖包装函数(含空指针、错误码翻译);C 侧或集成层用少量端到端用例验证真实调用路径。高频事故集中在三处——忘记 CString 属主提前 drop 导致悬垂指针、错误码没翻译成 Result 直接当成功、结构体两侧字段顺序改动不同步。第三处的防线是把 layout 测试固化为编译期断言(比较 mem::size_of 与约定值),字段顺序变更会直接编译失败,而不是运行期读到脏数据。
C 侧回调是 FFI 里最细的一环:函数指针穿越边界后,Rust 侧必须保证闭包生命周期覆盖回调可能被调用的窗口,且回调本身不 panic。
use std::ffi::c_int; extern "C" fn on_done(code: c_int, ctx: *mut std::ffi::c_void) { // extern "C" fn 是纯函数指针:不捕获环境,天然可传 C if let Some(boxed) = unsafe { Box::from_raw(ctx as *mut String) } { println!("回调 {}:{}", code, boxed); // 属主回收:ctx 原是 Box::into_raw 放过去的 } } extern "C" { fn ht_async_scan(cb: extern "C" fn(c_int, *mut std::ffi::c_void), ctx: *mut std::ffi::c_void); } fn main() { let ctx = Box::into_raw(Box::new(String::from("扫描任务"))) as *mut _; unsafe { ht_async_scan(on_done, ctx) }; // 属主经 into_raw 移交、经 from_raw 收回,指针的一生产权分明 }
这个往返判例把三件事钉死:上下文用 Box 的裸指针跨边界传递,属主两次移交首尾相接;回调用 extern "C" fn 而非闭包,捕获需求由 ctx 通道满足;回调体内不 panic,若内部逻辑可能失败,转成日志或错误码。FFI 的全部纪律浓缩起来就是这张属主流转图。