在 Rust 开发 DLL 时,正确处理 *mut c_char
和 *const c_char
与 String
的转换非常重要。以下是几种常见场景的安全转换方法:
1️⃣ C 字符串 → Rust String
将传入的 *const c_char
(C 风格以 null 结尾的字符串) 转换为 Rust 的 String
rust
use std::ffi::CStr;
use std::os::raw::c_char;
#[no_mangle]
pub extern "C" fn process_c_string(c_string_ptr: *const c_char) {
// 安全转换
let rust_str = unsafe {
// 注意:要求传入有效的 C 字符串指针 (null-terminated)
CStr::from_ptr(c_string_ptr)
.to_str() // 转换为 &str
.expect("Invalid UTF-8") // 处理错误
.to_string() // &str → String
};
println!("Rust received: {}", rust_str);
}
安全要点:
- 使用
CStr::from_ptr
包装 C 字符串 to_str()
处理 UTF-8 转换(可能会失败)- 必须进行错误处理(这里用
expect()
,生产环境建议更健壮的方案)
2️⃣ Rust String → C 字符串
将 Rust String 转换为可返回给 C 的 *mut c_char
rust
use std::ffi::CString;
#[no_mangle]
pub extern "C" fn generate_c_string() -> *mut c_char {
let rust_string = "从 Rust 生成的字符串".to_string();
// 创建 CString (可能失败)
let c_string = CString::new(rust_string)
.expect("Failed to create CString");
// 泄漏所有权,转为原始指针
c_string.into_raw()
}
// ⚠️ 必须提供对应的内存释放函数!
#[no_mangle]
pub extern "C" fn free_c_string(ptr: *mut c_char) {
unsafe {
if !ptr.is_null() {
// 回收所有权并自动释放内存
let _ = CString::from_raw(ptr);
}
}
}
关键点:
into_raw()
让 Rust 放弃所有权,C 代码需负责管理内存- 必须提供对应的
free_xxx
函数 - 注意字符串中的 null 字符会导致
CString::new()
失败
3️⃣ 完整 DLL 示例
结合转换的综合使用:
rust
// src/lib.rs
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
#[no_mangle]
pub extern "C" fn modify_string(input: *const c_char) -> *mut c_char {
// Step 1: C字符串 → Rust String
let rust_str = unsafe {
CStr::from_ptr(input)
.to_str()
.unwrap_or_default() // 更安全的错误处理
.to_uppercase() // 处理字符串
};
// Step 2: Rust String → C字符串
CString::new(rust_str)
.map(|s| s.into_raw())
.unwrap_or(std::ptr::null_mut())
}
#[no_mangle]
pub extern "C" fn free_modified_string(ptr: *mut c_char) {
if !ptr.is_null() {
unsafe {
let _ = CString::from_raw(ptr);
}
}
}
4️⃣ C/C++ 调用示例
如何在 C 中正确使用 DLL:
c
#include <stdio.h>
#include <windows.h>
typedef char* (*fn_modify_string)(const char*);
typedef void (*fn_free_string)(char*);
int main() {
HINSTANCE dll = LoadLibrary("rust_dll.dll");
if (!dll) return 1;
fn_modify_string modify = (fn_modify_string)GetProcAddress(dll, "modify_string");
fn_free_string free_str = (fn_free_string)GetProcAddress(dll, "free_modified_string");
// 调用函数
char* result = modify("hello from c");
printf("Modified: %s\n", result);
// 必须释放内存!
free_str(result);
FreeLibrary(dll);
return 0;
}
🧠 高级处理技巧
1. 处理无效指针和错误
rust
pub extern "C" fn safe_convert(ptr: *const c_char) -> Option<String> {
if ptr.is_null() {
return None;
}
unsafe {
CStr::from_ptr(ptr)
.to_str()
.ok()
.map(|s| s.to_string())
}
}
2. 多字节编码转换 (Windows)
rust
use encoding_rs::WINDOWS_1252;
pub fn to_wide_string(s: &str) -> Vec<u16> {
let (cow, _) = WINDOWS_1252.encode(s);
cow.iter()
.map(|b| *b as u16) // 单字节转宽字符
.chain(std::iter::once(0)) // 添加 null 终止符
.collect()
}
3. 返回复杂数据结构
rust
#[repr(C)]
pub struct StringResult {
data: *mut c_char,
success: bool
}
#[no_mangle]
pub extern "C" fn complex_operation() -> StringResult {
// ... 操作 ...
match result {
Ok(s) => StringResult {
data: CString::new(s).unwrap().into_raw(),
success: true
},
Err(_) => StringResult {
data: std::ptr::null_mut(),
success: false
}
}
}
⚠️ 关键安全警告
-
线程安全:
rustuse std::sync::Mutex; static BUFFER: Mutex<String> = Mutex::new(String::new());
-
空指针检查:
- 所有指针参数必须
is_null()
检查 - 返回指针要考虑无效情况
- 所有指针参数必须
-
禁止的字符串转换:
rust// 绝对禁止这种转换! unsafe { std::str::from_utf8_unchecked(slice) }
-
内存生命周期:
rust// 危险:局部变量内存被释放! pub extern "C" fn invalid_example() -> *const c_char { let s = String::from("temporary"); CString::new(s).unwrap().as_ptr() // 返回即将销毁的指针 }
💡 最佳实践总结
- 使用
CStr
/CString
进行安全的转换 - 明确所有权 :
into_raw()
转让所有权到 Cfrom_raw()
取回所有权释放内存
- 配套释放函数 :
- 每个
into_raw()
必须有对应的 free 函数
- 每个
- 错误处理 :
- 返回
Option
或带错误标识的结构体
- 返回
- 测试边界情况 :
- 空指针
- 无效 UTF-8
- 含 null 的字符串
完成开发后,建议使用 cbindgen 自动生成头文件:
bash
cargo install cbindgen
cbindgen --lang c --output mydll.h