Rust使用Box向自定义DLL传递堆数据为何随机出错?保留Bool枚举异常
问题描述
我需要将结构体数据传递给自行开发的DLL,为避免数据被释放,使用Box将结构体转移至堆内存,并通过Box::into_raw获取指针。但偶尔在DLL中打印数据时会随机出现错误,错误提示为“fatal runtime error: Rust cannot catch foreign exceptions”及栈缓冲区溢出,进程退出码为0xc0000409。
经排查发现,出错时数据的内存地址确实发生了变化:主程序中String值的地址为0x18f89dfe810,但DLL中读取到的地址变为0x18f89dfc2a0。
DLL代码
#[repr(C)] #[derive(Debug, Clone)] pub enum MyValue { Bool(bool), String(*const i8), } #[repr(C)] #[derive(Debug, Clone)] pub struct StoreData { values: *const MyValue, } #[no_mangle] extern "C" fn read_data(data: *const StoreData) { println!(" "); println!("==== in dll ===="); println!("01 {:?}", data); unsafe { println!("02 {:?}", *(*data).values); let MyValue::String(value) = *(*data).values.offset(0) else { panic!("Dll Error") }; println!("03 {:?}", value); }; // do something... }
主程序代码(使用libloading = "0.8.4")
use libloading::{Library, Symbol}; use std::ffi::CString; #[repr(C)] #[derive(Debug, Clone)] pub enum MyValue { Bool(bool), String(*const i8), } #[repr(C)] #[derive(Debug, Clone)] pub struct StoreData { values: *const MyValue, } #[repr(C)] #[derive(Debug, Clone)] pub struct EntryPoint {} impl EntryPoint { pub fn init(values: Vec<String>) -> StoreData { let mut container = Vec::new(); for var in values { let c_var = CString::new(var.as_str()).unwrap(); container.push(MyValue::String(c_var.into_raw() as *const i8)); } StoreData { values: container.as_ptr(), } } pub fn call_dll(values: *mut StoreData) { type DLLFUNC = extern "C" fn(data: *const StoreData); unsafe { let lib = Library::new("something_dll.dll").unwrap(); let func: Symbol<DLLFUNC> = lib.get(b"read_data").unwrap(); func(values); lib.close().unwrap() } } } fn main() { let values = vec![String::from("hello"), String::from("world")]; let sd = EntryPoint::init(values); let sd_addr = Box::into_raw(Box::new(sd)); println!("==== main func get data ===="); println!("01 {:?}", sd_addr); unsafe { println!("02 {:?}", *(*sd_addr).values); let MyValue::String(value) = *(*sd_addr).values.offset(0) else { panic!("main ERROR") }; println!("03 {:?}", value); }; EntryPoint::call_dll(sd_addr); unsafe { let _ = Box::from_raw(sd_addr); } }
当注释掉MyValue枚举中的Bool变体后,DLL中打印的值始终正确,但必须保留MyValue::Bool以传递布尔类型数据。原本认为Box是Rust中维护数据生命周期与所有权的可靠工具,无法理解该问题成因,恳请告知问题原因及代码优化方案。
问题原因分析
- 核心:悬垂指针:
EntryPoint::init函数中的container是局部栈分配的Vec<MyValue>,函数返回时container会被自动销毁,其内部堆内存被回收。但返回的StoreData中保存的values是container.as_ptr()获取的指针,此时该指针已变成悬垂指针。后续用Box包装StoreData本身,但StoreData里的values指针指向的内存已被释放,访问时内存可能被重新分配给其他对象,导致地址变化和非法访问,触发栈溢出或异常。 - CString所有权问题:
c_var.into_raw()将CString转为裸指针,但未在合适时机回收内存,既造成泄漏,也可能因内存被回收出现非法访问。 - 枚举布局的放大效应:
#[repr(C)]保证了C兼容的枚举布局,但存在多变体时,悬垂指针访问的非法内存可能被解析为错误变体,触发panic!。由于DLL和主程序是独立运行时,Rust无法跨边界捕获该panic,进而抛出致命错误。注释掉Bool变体后,非法内存访问可能恰好未触发变体匹配错误,暂时掩盖了问题,但悬垂指针的本质问题仍存在。
代码优化方案
1. 重构数据结构,明确内存所有权
让StoreData持有内存所有权,同时传递数据长度避免越界:
// 主程序与DLL共用的定义(建议抽成共享模块) #[repr(C)] #[derive(Debug, Clone)] pub enum MyValue { Bool(bool), String(*const i8), } #[repr(C)] #[derive(Debug)] pub struct StoreData { values: *const MyValue, len: usize, // 新增:传递元素数量 _vec_owner: *mut Vec<MyValue>, // 持有Vec的所有权,避免内存释放 _cstring_owner: *mut Vec<CString>, // 持有CString的所有权 }
2. 修改init函数,维护内存生命周期
impl EntryPoint { pub fn init(values: Vec<String>) -> Box<StoreData> { let mut container = Vec::new(); let mut c_strings = Vec::new(); for var in values { let c_var = CString::new(var.as_str()).unwrap(); container.push(MyValue::String(c_var.as_ptr())); c_strings.push(c_var); // 保留CString所有权,避免悬垂 } // 将Vec转移到堆上,用Box持有,避免局部变量销毁 let vec_box = Box::new(container); let cstring_box = Box::new(c_strings); Box::new(StoreData { values: vec_box.as_ptr(), len: vec_box.len(), _vec_owner: Box::into_raw(vec_box), _cstring_owner: Box::into_raw(cstring_box), }) } pub fn call_dll(sd: *mut StoreData) { type DLLFUNC = extern "C" fn(data: *const StoreData); unsafe { let lib = Library::new("something_dll.dll").unwrap(); let func: Symbol<DLLFUNC> = lib.get(b"read_data").unwrap(); func(sd); lib.close().unwrap() } } // 新增:显式回收所有内存 pub fn cleanup(sd: *mut StoreData) { unsafe { let store_data = &*sd; // 回收Vec<MyValue> let _ = Box::from_raw(store_data._vec_owner); // 回收Vec<CString> let _ = Box::from_raw(store_data._cstring_owner); // 回收StoreData本身 let _ = Box::from_raw(sd); } } }
3. 修改主程序main函数,正确调用
fn main() { let values = vec![String::from("hello"), String::from("world")]; let sd = EntryPoint::init(values); let sd_addr = Box::into_raw(sd); println!("==== main func get data ===="); println!("01 {:?}", sd_addr); unsafe { println!("02 {:?}", *(*sd_addr).values); let MyValue::String(value) = *(*sd_addr).values.offset(0) else { panic!("main ERROR") }; println!("03 {:?}", value); }; EntryPoint::call_dll(sd_addr); // 使用完后回收所有内存 EntryPoint::cleanup(sd_addr); }
4. 修改DLL的read_data函数,安全访问
#[no_mangle] extern "C" fn read_data(data: *const StoreData) { println!(" "); println!("==== in dll ===="); println!("01 {:?}", data); unsafe { let store_data = &*data; println!("02 total elements: {}", store_data.len); // 遍历所有元素,避免越界 for i in 0..store_data.len { let value = &*store_data.values.offset(i as isize); println!("element {}: {:?}", i, value); if let MyValue::String(s) = value { println!("string value: {:?}", std::ffi::CStr::from_ptr(*s)); } } }; // do something... }
关键优化点
- 杜绝悬垂指针:通过
Box持有Vec<MyValue>和Vec<CString>的所有权,确保DLL使用期间内存不会被释放。 - 明确数据边界:新增
len字段,让DLL知晓元素数量,避免越界访问。 - 显式内存回收:提供
cleanup函数,统一释放所有堆内存,避免泄漏。 - 安全枚举处理:用
if let替代else panic,避免因非法内存触发无法捕获的panic。
内容的提问来源于stack exchange,提问作者Jayden
相关产品推荐
相关产品推荐

