You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过FFI避免双重释放与内存泄漏问题

FFI场景下的内存安全问题:防止双重释放与内存泄漏

我有一个闭源C程序,无法获取源码,它可以加载Rust动态库执行额外处理。当前的核心问题是:在动态库处理过程中如何避免双重释放和内存泄漏。规则是:程序以指针形式传递结构体,无需使用时需调用指定函数释放;如果结构体返回给C主逻辑,由C负责跟踪指针并销毁;如果结构体不返回,则由Rust负责清理。

我写了一段代码看似可行,但不够优雅,现在有三个问题:

  1. 使用cast是否正确?
  2. 目前无法阻止多次调用get_interior,有没有办法避免?
  3. 这应该是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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 21:17:49