如何处理 C 字符串与 Rust 字符串的双向转换?
解读
在国内后端、嵌入式、区块链、云原生等 Rust 岗位面试中,“C 字符串 ↔ Rust 字符串” 是必考边界问题。面试官想确认三点:
- 是否理解** Rust 字符串的 UTF-8 不变性与内存布局**;
- 是否掌握** unsafe 边界与零拷贝权衡**;
- 能否在不引入 UB(未定义行为)的前提下,给出可工程落地的健壮代码(含错误处理、空指针、非法 UTF-8、生命周期标注)。
知识点
-
Rust 侧类型
String:堆分配、可变、UTF-8。&str:胖指针(ptr + len),UTF-8。*const c_char / *mut c_char:C 侧裸指针,以\0结尾,单字节或平台编码,不保证 UTF-8。
-
C 侧约定
- 传入 Rust:指针可能为 NULL,可能非 UTF-8。
- 传出 Rust:调用方最终须调用配套 free 函数(谁分配谁释放原则)。
-
核心 API
std::ffi::{CStr, CString, OsStr, OsString}std::os::raw::c_charstd::slice::from_raw_parts(unsafe)libc::strlen(需 libc 依赖,嵌入式可自写)
-
安全边界
- NULL 检查先于任何解引用。
- UTF-8 校验:
CStr::to_str返回Result,必须处理。 - 生命周期:Rust 不能持有 C 传入的指针超过函数返回边界,除非显式拷贝。
- 分配器一致性:Rust 默认使用 jemalloc/system malloc;若 C 侧用自定义 malloc,禁止直接 CString::into_raw 后由 C 侧 free。
答案
下面给出双向转换的完整工程模板,可直接写进白板,也可在 IDE 中跑通,符合国内代码审计标准。
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
/// C -> Rust:将 C 字符串转为 Rust &str
/// # Safety
/// 调用方保证 `ptr` 为合法以 `\0` 结尾的字符串,或 NULL。
/// 返回 `None` 表示空指针或非法 UTF-8。
#[no_mangle]
pub unsafe extern "C" fn c_to_rust_str(ptr: *const c_char) -> Option<String> {
if ptr.is_null() {
return None;
}
// 用 CStr::from_ptr 做边界检查,内部调 strlen
let c_str = unsafe { CStr::from_ptr(ptr) };
// 显式 UTF-8 校验,拒绝非法字节序列
match c_str.to_str() {
Ok(s) => Some(s.to_owned()), // 拷贝到 Rust 侧,生命周期安全
Err(_) => None,
}
}
/// Rust -> C:将 Rust &str 转出给 C 调用方
/// # Safety
/// 返回指针由 Rust 分配,C 侧使用完后**必须**调用 `rust_string_free`。
/// 返回 NULL 表示 Rust 侧传入空串或分配失败。
#[no_mangle]
pub unsafe extern "C" fn rust_to_c_str(rust: *const c_char) -> *mut c_char {
if rust.is_null() {
return std::ptr::null_mut();
}
// 先还原为 &str
let c_slice = unsafe { CStr::from_ptr(rust) };
let utf8 = match c_slice.to_str() {
Ok(s) => s,
Err(_) => return std::ptr::null_mut(),
};
// 分配 CString
match CString::new(utf8) {
Ok(c_string) => c_string.into_raw(), // 所有权转移给 C
Err(_) => std::ptr::null_mut(), // 中间含 \0,非法
}
}
/// C 侧调用完后,必须将指针传回 Rust 释放
/// # Safety
/// `ptr` 必须是由 `rust_to_c_str` 返回的非空指针。
#[no_mangle]
pub unsafe extern "C" fn rust_string_free(ptr: *mut c_char) {
if ptr.is_null() {
return;
}
// 重新取得 CString 所有权,drop 时自动释放
unsafe {
let _ = CString::from_raw(ptr);
}
}
使用约束总结:
- C 侧拿到
*mut c_char后,禁止再写入,否则破坏内部\0结尾约定。 - 禁止 C 侧直接
free(ptr),必须走rust_string_free,保证同一分配器。 - 若高频调用,可额外提供零拷贝版本:Rust 侧返回结构体
{ ptr: *const c_char, len: usize },但需约定只读、生命周期不超过下一次 Rust 函数返回,并文档注明。
拓展思考
-
非 UTF-8 场景(GBK、Latin-1):
国内老系统常用 GBK,此时CStr::to_str必失败。可引入encoding_rs库,在c_to_rust_str内做 GBK→UTF-8 转码,转码失败返回None,保证 Rust 内部永远 UTF-8。 -
零拷贝双向视图(高级):
若 C 侧保证内存只读且生命周期可控,可返回&str的胖指针结构体,避免String::to_owned拷贝;但需在 Rust 侧用 PhantomData 标注生命周期,防止悬垂。面试中提及即可体现深度。 -
错误码 vs Option:
国内部分安全规范要求错误码枚举而非 NULL。可把返回值改成Result<String, CConvertError>的 FFI 安全版本,通过 out-param 返回错误码,满足车载、金融审计要求。 -
WASM 嵌入前端:
若 Rust 编译到wasm32-unknown-unknown,CString::into_raw得到的指针属于 WASM Linear Memory,JS 侧需导出 rust_string_free 并在同一模块实例内调用,否则出现重复 free 或内存泄漏。 -
性能极致:
对热路径(如网络包解析)可预分配Vec<u8>作为缓冲区,用std::slice::from_raw_parts_mut让 C 侧直接写入,再在外部做一次性 UTF-8 校验,避免多次分配;但代码复杂度与 UB 风险同步升高,面试时权衡表达即可。