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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 22:15:53