10.2 FFI与no_std:跨境协查


10.2 FFI 与 no_std:跨境协查

本节摘要:FFI 让 Rust 与 C 互相调用——extern 声明外来函数、CString 打磨字符串边界;no_std 则砍掉标准库,把 Rust 带进裸机与嵌入式。两条通道都建立在上一节的越权框架上。读完你能读懂并写出简单的跨语言接口,理解 Rust 进军嵌入式与内核的路径。

FFI:引渡条款

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:去标准库办案

#![no_std] // 放弃标准库,只留核心库 core fn add(a: i32, b: i32) -> i32 { a + b }

标准库带着堆分配、线程、网络;嵌入式与内核模块没有这些。no_std 项目只依赖 core(那些不需要操作系统的部分:迭代器、Option、Trait 全在),堆要自己接分配器。第 3 章的所有权法条在裸机上原封不动——这正是 Rust 能进嵌入式与操作系统领域的资格证明。

要点回顾

  • extern 声明加 unsafe 调用是引渡 C 函数的标准程序;
  • 字符串与指针在边界打磨,CString 与复制优先;
  • 导出给 C 用 no_mangle,双向通道都通;
  • no_std 保留全部内存安全法条,只裁掉运行时设施。

extern 块:跨境声明的完整判例

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:裁掉标准运行时

#![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 的全部纪律浓缩起来就是这张属主流转图。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U