Rust FFI封装层如何捕获可变性错误避免未定义行为
FFI厚封装层的可变性强制校验方案
核心问题
对接C库编写FFI安全封装时,直接存储*mut T裸指针存在严重风险:裸指针的可变性不参与Rust借用检查,即使在&self的共享引用方法中,也可以无警告地取出*mut T传给会修改底层数据的C接口。如果C侧API将参数从*const T修改为*mut T,Rust侧不会给出任何编译提示,极易触发未定义行为。
此前尝试的几种方案均存在适配问题:
- 直接存裸指针:无任何编译期可变性校验
- 使用
Box<T>:Box::from_raw会接管C分配内存的所有权,Rust侧自动析构时会和C侧的释放函数冲突,触发double free - 使用
Cell<T>/RefCell<T>:提供内部可变性,绕过Rust借用规则,允许通过共享引用修改数据,无法达到所有权校验的目的
实现思路
使用零成本的NonNull<T>类型替代裸指针存储C侧返回的指针,将指针获取逻辑和Rust方法的&self/&mut self签名严格绑定:
- 仅在
&mut self的可变方法中,才能取出*mut T传给需要修改数据的C接口 - 仅在
&self的共享方法中,才能取出*const T传给只读的C接口
这种方式完全不干预C侧的内存管理逻辑,所有内存分配、释放都由C侧API负责,同时可以让Rust类型检查器强制校验可变性:如果C侧API签名从*const T改为*mut T,原有传参位置会直接报类型不匹配错误,必须同步修改Rust侧方法的借用签名才能编译通过,从根源上避免UB。
完整实现代码
use std::ptr::{null_mut, NonNull}; #[repr(C)] pub struct c_thing { _unused: [u8; 0], } extern "C" { pub fn c_thing_init(thing: *mut *mut c_thing); pub fn c_thing_mutate(thing: *mut c_thing); pub fn c_thing_release(thing: *mut c_thing); } struct CThingWrapper { // NonNull和裸指针内存布局完全一致,零开销,同时保证指针非空 thing: NonNull<c_thing>, } impl CThingWrapper { pub fn new() -> Self { let mut thing: *mut c_thing = null_mut(); unsafe { c_thing_init(&mut thing) }; Self { thing: NonNull::new(thing).expect("C API returned null pointer on init"), } } // 可变方法签名对应C侧*mut参数要求,必须持有可变引用才能调用 pub fn mutate(&mut self) { unsafe { c_thing_mutate(self.thing.as_ptr()); } } // 只读方法示例:共享引用签名对应C侧*const参数要求 // pub fn get_value(&self) -> i32 { // unsafe { c_thing_get_value(self.thing.as_ptr() as *const c_thing) } // } } impl Drop for CThingWrapper { fn drop(&mut self) { unsafe { // 完全委托C侧释放资源,Rust侧不做任何内存回收操作 c_thing_release(self.thing.as_ptr()); } } } // 根据C库实际的线程安全保证手动实现Send/Sync trait unsafe impl Send for CThingWrapper {} unsafe impl Sync for CThingWrapper {} fn main() { // 必须声明为mut绑定才能调用可变方法,符合Rust所有权规则 let mut x = CThingWrapper::new(); x.mutate(); }
扩展场景适配
如果部分C侧API会转移指针所有权(比如调用某个接口后C侧会自动释放资源,不需要Rust侧再调用release),可以将字段类型改为Option<NonNull<c_thing>>:
- 指针有效时存储
Some(NonNull<...>) - 所有权转移给C侧后赋值为
None - Drop时判断如果是
Some才调用C侧释放函数,避免double free
这种方式和Option<Box<T>>的思路一致,但完全不会接管C侧内存管理,适配FFI场景。
方案优势
- 零运行时开销:
NonNull和裸指针的内存表示完全相同,没有任何额外性能损耗 - 编译期强校验:C侧API可变性变更会直接触发编译错误,强制开发者同步检查Rust侧API的所有权语义,避免共享引用修改数据的UB
- 内存管理透明:Rust侧不干预C侧的内存分配释放逻辑,不会出现重复释放、内存泄漏问题
- 完全收拢unsafe逻辑:所有FFI相关的unsafe代码都被封装在Wrapper内部,对外暴露完全安全的Rust API,符合厚封装层的设计要求
内容的提问来源于stack exchange,提问作者Evan Benn
相关产品推荐
相关产品推荐

