Rust:DLL 输出对象的生命周期管理

发布于:2025-08-18 ⋅ 阅读:(13) ⋅ 点赞:(0)

在 Rust 开发 DLL 时安全地将对象地址传递给 C 语言并保持对象不被析构,需要正确处理所有权和生命周期。以下是详细方案和代码示例:

核心思路

  1. 堆分配对象 - 使用 Box 在堆上分配对象
  2. 泄漏所有权 - 使用 Box::into_raw 防止 Rust 自动析构
  3. 明确所有权协议 - 提供销毁函数由 C 语言调用方管理生命周期
  4. 线程安全 - 使用 Send + Sync 确保跨线程安全

完整示例

Rust DLL 实现
// src/lib.rs
use std::sync::Mutex;

#[repr(C)]
pub struct Counter {
    value: Mutex<i32>,  // 内部可变性保证线程安全
}

impl Counter {
    pub fn new(init: i32) -> Self {
        Counter {
            value: Mutex::new(init),
        }
    }

    pub fn increment(&self) {
        let mut lock = self.value.lock().unwrap();
        *lock += 1;
    }

    pub fn get_value(&self) -> i32 {
        *self.value.lock().unwrap()
    }
}

// 创建对象并返回指针 (所有权转移给调用方)
#[no_mangle]
pub extern "C" fn counter_create(init: i32) -> *mut Counter {
    Box::into_raw(Box::new(Counter::new(init)))
}

// 增加计数器值
#[no_mangle]
pub extern "C" fn counter_increment(counter: *mut Counter) {
    assert!(!counter.is_null());
    unsafe { &*counter }.increment();
}

// 获取当前值
#[no_mangle]
pub extern "C" fn counter_get_value(counter: *const Counter) -> i32 {
    assert!(!counter.is_null());
    unsafe { &*counter }.get_value()
}

// 销毁对象(必须由调用方显式调用)
#[no_mangle]
pub extern "C" fn counter_destroy(counter: *mut Counter) {
    if !counter.is_null() {
        unsafe { Box::from_raw(counter) }; // 转为 Box 后立即析构
    }
}
C 语言调用示例
#include <stdio.h>
#include <windows.h>

typedef void* CounterHandle;

// 声明 DLL 函数
CounterHandle counter_create(int init);
void counter_increment(CounterHandle counter);
int counter_get_value(CounterHandle counter);
void counter_destroy(CounterHandle counter);

int main() {
    HMODULE dll = LoadLibrary("counter.dll");
    if (!dll) {
        fprintf(stderr, "Failed to load DLL\n");
        return 1;
    }

    // 获取函数指针
    CounterHandle (*create)(int) = (CounterHandle(*)(int))GetProcAddress(dll, "counter_create");
    void (*increment)(CounterHandle) = (void(*)(CounterHandle))GetProcAddress(dll, "counter_increment");
    int (*get_value)(CounterHandle) = (int(*)(CounterHandle))GetProcAddress(dll, "counter_get_value");
    void (*destroy)(CounterHandle) = (void(*)(CounterHandle))GetProcAddress(dll, "counter_destroy");

    // 创建计数器对象
    CounterHandle counter = create(10);
    
    // 使用计数器
    increment(counter);
    printf("Current value: %d\n", get_value(counter));  // 输出 11
    
    increment(counter);
    printf("Current value: %d\n", get_value(counter));  // 输出 12

    // 销毁对象
    destroy(counter);
    
    FreeLibrary(dll);
    return 0;
}

关键安全措施

  1. 所有权转移协议

    • counter_create → 将所有权转移给 C 调用方
    • counter_destroy → C 调用方交回所有权,触发析构
    • 所有其他操作(increment/get_value)只借用不获取所有权
  2. 空指针检查

    assert!(!counter.is_null());
    

    防止空指针解引用(在实际项目中可能需要更健壮的错误处理)

  3. 线程安全保证

    use std::sync::Mutex;
    

    使用互斥锁确保内部可变性是线程安全的

  4. 内存安全

    • Box::into_raw 转换后,Rust 停止管理生命周期
    • Box::from_raw 恢复所有权后立即析构
  5. FFI 安全

    #[repr(C)]
    pub struct Counter { ... }
    

    保证 C 兼容的内存布局

生命周期管理建议

  1. 强制销毁协议

    • 在文档中明确要求 C 调用方必须调用 counter_destroy
    • 可使用 RAII 封装(如 C++ 的 unique_ptr)管理对象生命周期
  2. 避免跨 DLL 边界析构

    • 所有内存操作应在同一 DLL 模块中进行
    • 建议对象创建和销毁都在 Rust DLL 中完成
  3. 版本兼容

    • 当修改对象结构时:
    #[repr(C)]
    pub struct CounterV2 {
        // 不要修改现有字段顺序
        value: Mutex<i32>,
        // 新增字段永远放在末尾
        max_value: i32, 
    }
    

替代方案:引用计数

如果需要共享所有权,可考虑 Arc

#[no_mangle]
pub extern "C" fn counter_create_arc(init: i32) -> *const Counter {
    Arc::into_raw(Arc::new(Counter::new(init)))
}

#[no_mangle]
pub extern "C" fn counter_clone(counter: *const Counter) -> *const Counter {
    unsafe { Arc::increment_strong_count(counter) };
    counter
}

#[no_mangle]
pub extern "C" fn counter_destroy_arc(counter: *const Counter) {
    unsafe { Arc::decrement_strong_count(counter) };
}

警告:使用引用计数时,调用方必须严格配对 clone/destroy 调用

最佳实践

  1. 使用 abi_stable - 提供更稳定的 ABI 接口
  2. 自动化测试 - 使用 cbindgen 生成头文件并编写 C/C++ 测试
  3. 内存分析 - 使用 Valgrind 或 Windows CRT 调试堆检测内存泄漏

通过这种方式,您可以安全地将 Rust 对象传递给 C,同时保持明确的所有权和生命周期管理。


网站公告

今日签到

点亮在社区的每一天
去签到