如何通过FFI避免双重释放与内存泄漏问题
我有一个闭源C程序,无法获取源码,它可以加载Rust动态库执行额外处理。当前的核心问题是:在动态库处理过程中如何避免双重释放和内存泄漏。规则是:程序以指针形式传递结构体,无需使用时需调用指定函数释放;如果结构体返回给C主逻辑,由C负责跟踪指针并销毁;如果结构体不返回,则由Rust负责清理。
我写了一段代码看似可行,但不够优雅,现在有三个问题:
- 使用
cast是否正确? - 目前无法阻止多次调用
get_interior,有没有办法避免? - 这应该是FFI中很常见的场景,有没有更优的实现方式或标准模式?
use std::mem::ManuallyDrop; #[repr(C)] pub struct FFI_Object { r: i32, value: f64, } pub struct Wrapper { consumed: bool, interior: ManuallyDrop<*mut FFI_Object>, } impl Wrapper { pub fn new(x: *mut FFI_Object) -> Self { Self { consumed: false, interior: ManuallyDrop::new(x), } } pub fn get_interior(&mut self) -> *mut FFI_Object { self.consumed = true; self.interior.cast() // again not sure if cast is correct } } impl Drop for Wrapper { fn drop(&mut self) { unsafe { if !self.consumed { free(self.interior.cast()); // is cast() right here? } } } } #[no_mangle] pub unsafe extern "C" fn do_leak() -> *mut FFI_Object { let m = new_object(); // Memory leak as C program won't free let n = new_object(); n // C takes control and will free when needed } #[no_mangle] pub unsafe extern "C" fn do_double_free() -> *mut FFI_Object { let m = new_object(); free(m); m // C takes control and will cause a double free } #[no_mangle] pub unsafe extern "C" fn do_no_leak() -> *mut FFI_Object { let m = Wrapper::new(new_object()); // Drop called immediately and freed let mut n = Wrapper::new(new_object()); // mut feels wrong n.get_interior() // Rust wont drop because "consumed", C will handle dropping } extern "C" { pub fn new_object() -> *mut FFI_Object; pub fn free(x: *mut FFI_Object); }
问题1:cast的使用是否正确?
你的cast调用是多余但无害的。因为interior本身就是*mut FFI_Object类型,cast()在这里没有做任何实际类型转换(相当于冗余的as *mut FFI_Object)。直接解引用ManuallyDrop拿到指针即可,不需要cast():
// get_interior中 *self.interior // Drop实现中 free(*self.interior);
问题2:如何避免多次调用get_interior?
当前的consumed标记只能记录状态,无法从根本上阻止重复调用。可以通过状态机枚举设计Wrapper,彻底区分“未消耗”和“已消耗”状态:
pub enum Wrapper { Owned(*mut FFI_Object), Consumed, } impl Wrapper { pub fn new(x: *mut FFI_Object) -> Self { Wrapper::Owned(x) } pub fn get_interior(&mut self) -> *mut FFI_Object { match std::mem::replace(self, Wrapper::Consumed) { Wrapper::Owned(ptr) => ptr, Wrapper::Consumed => panic!("Cannot call get_interior on a consumed Wrapper"), } } } impl Drop for Wrapper { fn drop(&mut self) { if let Wrapper::Owned(ptr) = self { unsafe { free(*ptr); } } } }
第一次调用get_interior后,Wrapper会切换为Consumed状态,后续调用会直接panic。如果不想panic,也可以返回Option<*mut FFI_Object>或Result,让调用者优雅处理重复调用的情况。
另外,你觉得mut n别扭是正常的,但这里mut是必须的——因为要修改Wrapper的状态,转移所有权必然需要可变引用。
问题3:FFI中这类场景的标准模式
这是FFI中典型的所有权边界划分场景,核心思路是用RAII包装器(你的Wrapper本质就是RAII)明确内存责任:
- Rust接管C分配的内存时,用RAII包装器持有,确保
Drop时自动释放(除非所有权转移给C)。 - 需要将内存交还给C时,通过方法转移所有权,同时标记包装器不再负责释放。
除了自定义实现,也可以用更简洁的方式:比如用Option代替自定义枚举,通过take()方法转移所有权:
pub struct Wrapper { interior: Option<*mut FFI_Object>, } impl Wrapper { pub fn new(x: *mut FFI_Object) -> Self { Self { interior: Some(x) } } pub fn get_interior(&mut self) -> Option<*mut FFI_Object> { self.interior.take() } } impl Drop for Wrapper { fn drop(&mut self) { if let Some(ptr) = self.interior.take() { unsafe { free(ptr); } } } }
这种方式下,get_interior第一次调用返回Some(ptr),后续调用返回None,调用者可以直接判断是否已被消耗,避免错误操作。
另外,你do_no_leak函数中let m = Wrapper::new(new_object());的写法非常合理——m未被使用,会在该行结束时自动触发Drop,由Rust清理未返回给C的对象,完全符合内存规则。
内容的提问来源于stack exchange,提问作者Peter

