为C库实现Rust封装时避免Drop重复调用引发未定义行为
解决Rust封装C库时的双重释放问题
问题背景
我正在为VMAF这个C库构建Rust封装。C库的结构体通常配套内存分配与释放函数,原本计划在Rust封装类型的构造函数中调用C的分配逻辑,在Drop trait中执行释放操作。但遇到一个问题:部分C函数(比如vmaf_read_pictures)会在内部调用释放函数消耗传入的结构体,当Rust封装实例超出作用域时,Drop中的释放代码会被二次执行,引发未定义行为。
解决方案
方案1:用std::mem::ManuallyDrop手动管控生命周期
ManuallyDrop可以禁用Rust的自动Drop调用,让你完全手动控制释放时机。当调用会消耗结构体的C函数后,可通过ManuallyDrop::into_inner取出内部值,避免后续Drop触发。
示例Rust代码:
use std::mem::ManuallyDrop; #[repr(C)] pub struct VmafPicture { pub pix_fmt: u32, pub bpc: u32, pub w: [u32; 3], pub h: [u32; 3], pub stride: [isize; 3], pub data: [*mut u8; 3], pub ref_: *mut (), // 对应C的VmafRef* } pub struct VmafPictureWrapper { inner: ManuallyDrop<VmafPicture>, } impl VmafPictureWrapper { // 构造函数:调用C的分配函数 pub fn new(pix_fmt: u32, bpc: u32, w: u32, h: u32) -> Result<Self, i32> { let mut pic = unsafe { std::mem::zeroed::<VmafPicture>() }; let ret = unsafe { vmaf_picture_alloc(&mut pic, pix_fmt, bpc, w, h) }; if ret != 0 { return Err(ret); } Ok(Self { inner: ManuallyDrop::new(pic) }) } // 获取内部指针供C函数调用 pub fn as_ptr(&mut self) -> *mut VmafPicture { &mut *self.inner } // 手动释放:确认需要自行释放时调用 pub fn unref(mut self) -> Result<(), i32> { let ret = unsafe { vmaf_picture_unref(&mut *self.inner) }; let _ = ManuallyDrop::into_inner(self.inner); // 取出内部值,避免Drop触发 ret.eq(&0).then_some(()).ok_or(ret) } } impl Drop for VmafPictureWrapper { fn drop(&mut self) { // 检查ref_是否为空,因为vmaf_picture_unref会把结构体memset为0 if !self.inner.ref_.is_null() { let _ = unsafe { vmaf_picture_unref(&mut *self.inner) }; } } } // 使用示例:调用vmaf_read_pictures fn use_vmaf_read_pictures( vmaf: *mut (), mut ref_pic: VmafPictureWrapper, mut dist_pic: VmafPictureWrapper ) -> Result<(), i32> { let ret = unsafe { vmaf_read_pictures(vmaf, ref_pic.as_ptr(), dist_pic.as_ptr(), 0) }; if ret != 0 { return Err(ret); } // C函数已释放图片,Drop时会自动跳过释放逻辑 Ok(()) }
方案2:内部添加所有权标志位
在封装结构体中加入owned布尔标志,标记当前Rust实例是否拥有C结构体的所有权。当调用会消耗结构体的C函数后,将owned设为false,Drop时仅在owned为true时执行释放。
示例Rust代码:
#[repr(C)] pub struct VmafPicture { // 同方案1字段定义 } pub struct VmafPictureWrapper { inner: VmafPicture, owned: bool, } impl VmafPictureWrapper { pub fn new(pix_fmt: u32, bpc: u32, w: u32, h: u32) -> Result<Self, i32> { let mut pic = unsafe { std::mem::zeroed::<VmafPicture>() }; let ret = unsafe { vmaf_picture_alloc(&mut pic, pix_fmt, bpc, w, h) }; if ret != 0 { return Err(ret); } Ok(Self { inner: pic, owned: true }) } pub fn as_ptr(&mut self) -> *mut VmafPicture { &mut self.inner } // 标记结构体已被C函数消耗,放弃所有权 pub fn mark_consumed(&mut self) { self.owned = false; } } impl Drop for VmafPictureWrapper { fn drop(&mut self) { if self.owned && !self.inner.ref_.is_null() { let _ = unsafe { vmaf_picture_unref(&mut self.inner) }; } } } // 使用示例 fn use_vmaf_read_pictures( vmaf: *mut (), mut ref_pic: VmafPictureWrapper, mut dist_pic: VmafPictureWrapper ) -> Result<(), i32> { let ret = unsafe { vmaf_read_pictures(vmaf, ref_pic.as_ptr(), dist_pic.as_ptr(), 0) }; if ret != 0 { return Err(ret); } // 标记图片已被消耗 ref_pic.mark_consumed(); dist_pic.mark_consumed(); Ok(()) }
方案3:结合C库的引用计数特性
从vmaf_picture_unref的实现可以看到,它内部使用引用计数管理内存,只有当计数归0时才会真正释放内存。但需要注意:vmaf_picture_unref最后会把结构体memset为0,即使计数未归0,结构体内容也会被清空,后续访问会触发未定义行为。因此该方案仍需结合前面的所有权管控逻辑使用,仅作为辅助验证手段。
相关C代码参考
结构体定义(picture.h)
#ifndef __VMAF_PICTURE_H__ #define __VMAF_PICTURE_H__ #include <stddef.h> #ifdef __cplusplus extern "C" { #endif enum VmafPixelFormat { VMAF_PIX_FMT_UNKNOWN, VMAF_PIX_FMT_YUV420P, VMAF_PIX_FMT_YUV422P, VMAF_PIX_FMT_YUV444P, VMAF_PIX_FMT_YUV400P, }; typedef struct VmafRef VmafRef; typedef struct { enum VmafPixelFormat pix_fmt; unsigned bpc; unsigned w[3], h[3]; ptrdiff_t stride[3]; void *data[3]; VmafRef *ref; } VmafPicture; int vmaf_picture_alloc(VmafPicture *pic, enum VmafPixelFormat pix_fmt, unsigned bpc, unsigned w, unsigned h); // 为VmafPicture分配内存 int vmaf_picture_unref(VmafPicture *pic); // 释放VmafPicture的内存 #ifdef __cplusplus } #endif #endif /* __VMAF_PICTURE_H__ */
触发问题的函数(vmaf_read_pictures)
int vmaf_read_pictures(VmafContext *vmaf, VmafPicture *ref, VmafPicture *dist, unsigned index) { if (!vmaf) return -EINVAL; if (vmaf->flushed) return -EINVAL; if (!ref != !dist) return -EINVAL; if (!ref && !dist) return flush_context(vmaf); int err = 0; vmaf->pic_cnt++; err = validate_pic_params(vmaf, ref, dist); if (err) return err; if (vmaf->thread_pool) return threaded_read_pictures(vmaf, ref, dist, index); for (unsigned i = 0; i < vmaf->registered_feature_extractors.cnt; i++) { VmafFeatureExtractorContext *fex_ctx = vmaf->registered_feature_extractors.fex_ctx[i]; if ((vmaf->cfg.n_subsample > 1) && (index % vmaf->cfg.n_subsample) && !(fex_ctx->fex->flags & VMAF_FEATURE_EXTRACTOR_TEMPORAL)) { continue; } err = vmaf_feature_extractor_context_extract(fex_ctx, ref, NULL, dist, NULL, index, vmaf->feature_collector); if (err) return err; } // 此处会释放传入的图片内存! err = vmaf_picture_unref(ref); if (err) return err; err = vmaf_picture_unref(dist); if (err) return err; return 0; }
vmaf_picture_unref实现
int vmaf_picture_unref(VmafPicture *pic) { if (!pic) return -EINVAL; if (!pic->ref) return -EINVAL; vmaf_ref_fetch_decrement(pic->ref); if (vmaf_ref_load(pic->ref) == 0) { aligned_free(pic->data[0]); vmaf_ref_close(pic->ref); } memset(pic, 0, sizeof(*pic)); return 0; }
内容的提问来源于stack exchange,提问作者Brandon Piña
相关产品推荐
相关产品推荐

