Rust FFI 包装推理库:unsafe 边界要像防火墙一样清楚

📅 2026/7/3 2:08:47
Rust FFI 包装推理库:unsafe 边界要像防火墙一样清楚
Rust FFI 包装推理库unsafe 边界要像防火墙一样清楚很多高性能推理库是 C/C 写的Rust 服务要复用它们就绕不开 FFI。FFI 本身没问题问题在于把 unsafe 扩散到业务代码里。指针生命周期、内存释放、线程安全、错误码转换任何一处没封好都可能把 Rust 的安全边界打穿。我的原则是unsafe 只出现在最小封装层业务层拿到的是安全 API。unsafe 边界要像防火墙一样清楚不能到处漏风。一、先定义 C 接口的所有权FFI 最怕所有权说不清。谁分配谁释放返回指针能活多久调用是否线程安全这些都要写进接口约定。flowchart TD A[Rust Safe API] -- B[FFI Wrapper] B -- C[unsafe extern call] C -- D[C/C Runtime] B -- E[错误码转换] B -- F[Drop 释放资源]Rust wrapper 的任务就是把不可靠的边界收窄并把约定固化成类型。二、用 RAII 管理句柄C 库常返回 handleRust 里应该用结构体包起来并在 Drop 中释放。pub struct ModelHandle { raw: NonNullc_void, } impl Drop for ModelHandle { fn drop(mut self) { unsafe { ffi_model_destroy(self.raw.as_ptr()) } } }这里 unsafe 仍然存在但范围很小。调用方不能忘记释放也不能随便拿 raw pointer 玩。三、输入输出要检查长度传 tensor buffer 时长度、对齐、dtype 都要检查。不要相信调用方。pub fn run(self, input: [f32], output: mut [f32]) - Result() { if input.len() ! self.input_len { return Err(Error::InvalidInputShape); } let code unsafe { ffi_model_run(self.raw.as_ptr(), input.as_ptr(), output.as_mut_ptr()) }; Error::from_code(code) }这类检查看起来啰嗦但它把崩溃变成了可处理错误。系统级代码最怕相信上游。输入验证的边界不止于 len 检查。在推理场景中tensor buffer 可能来自共享内存、DMA 区域或另一进程的 mmap对齐要求往往比标准 malloc 严格——比如 256 字节对齐用于 GPU DMA 传输。如果 FFI 层不校验对齐kernel launch 会在 CUDA 内部静默失败或产生错位结果。另一个容易被忽略的检查是 dtype 兼容性下游 C 库期望 f32但 Rust 侧传入了从 bf16 字节重解释的[f32]Slice 不会报错但计算结果完全错误。建议在 FFI 边界的前置校验中加入 alignment checkptr as usize % required_alignment 0和 dtype 标签校验用枚举而非裸整数传递数据类型让编译器帮你挡掉类型不匹配。对于 GPU 侧的 pinned memory 输入还要验证指针是否确实在 pinned 区域——这可以通过cudaPointerGetAttributes查询避免 kernel 内部因非 pinned 内存的隐式拷贝导致延迟陡增。除了输入校验输出 buffer 的治理同样重要C 库写入的 output tensor 若有越界写行为Rust 侧难以检测建议在 debug 编译时用 canary page 或 AddressSanitizer 包裹输出 buffer捕捉越界写生产环境则在 wrapper 层加入 output 校验和定期抽样比对发现异常立即告警并隔离对应 handle防止错误结果污染业务决策。四、线程安全要显式声明不是所有 C handle 都能跨线程。Rust 的Send、Sync不能随便 unsafe impl。只有确认底层库线程安全才能声明。如果底层不支持并发就在 wrapper 里加 Mutex 或要求每线程一个 handle。不要为了通过编译器把不确定性塞进unsafe impl Send。FFI 还要处理 panic 边界。Rust panic 不能跨过 C ABI 边界C exception 也不能随便穿进 Rust。回调函数尤其要小心必要时用catch_unwind把 panic 转成错误码。let result std::panic::catch_unwind(|| { user_callback(token_id) }); if result.is_err() { return FFI_CALLBACK_PANIC; }边界代码要宁可啰嗦也不要让未定义行为混进推理服务。五、总结Rust FFI 包装推理库时unsafe 边界要小、清楚、可审查。所有权、Drop、长度检查、错误码、线程安全都要在封装层处理。Rust 的安全不是自动延伸到 C 库里的。边界守住Rust 才能继续帮你挡 Bug。