在 Rust 开发 DLL 时安全地将对象地址传递给 C 语言并保持对象不被析构,需要正确处理所有权和生命周期。以下是详细方案和代码示例:
核心思路
- 堆分配对象 - 使用
Box
在堆上分配对象 - 泄漏所有权 - 使用
Box::into_raw
防止 Rust 自动析构 - 明确所有权协议 - 提供销毁函数由 C 语言调用方管理生命周期
- 线程安全 - 使用
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;
}
关键安全措施
所有权转移协议
counter_create
→ 将所有权转移给 C 调用方counter_destroy
→ C 调用方交回所有权,触发析构- 所有其他操作(
increment
/get_value
)只借用不获取所有权
空指针检查
assert!(!counter.is_null());
防止空指针解引用(在实际项目中可能需要更健壮的错误处理)
线程安全保证
use std::sync::Mutex;
使用互斥锁确保内部可变性是线程安全的
内存安全
Box::into_raw
转换后,Rust 停止管理生命周期Box::from_raw
恢复所有权后立即析构
FFI 安全
#[repr(C)] pub struct Counter { ... }
保证 C 兼容的内存布局
生命周期管理建议
强制销毁协议
- 在文档中明确要求 C 调用方必须调用
counter_destroy
- 可使用 RAII 封装(如 C++ 的
unique_ptr
)管理对象生命周期
- 在文档中明确要求 C 调用方必须调用
避免跨 DLL 边界析构
- 所有内存操作应在同一 DLL 模块中进行
- 建议对象创建和销毁都在 Rust DLL 中完成
版本兼容
- 当修改对象结构时:
#[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 调用
最佳实践
- 使用
abi_stable
库 - 提供更稳定的 ABI 接口 - 自动化测试 - 使用
cbindgen
生成头文件并编写 C/C++ 测试 - 内存分析 - 使用
Valgrind
或 Windows CRT 调试堆检测内存泄漏
通过这种方式,您可以安全地将 Rust 对象传递给 C,同时保持明确的所有权和生命周期管理。