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

Rust str与ffi::CString互转时字符串部分损坏问题排查

我来帮你排查这个问题——你遇到的是悬垂指针导致的未定义行为,具体原因和修复方案如下:

问题根源

在你的str::to_c_str实现里,你创建了一个CString实例result,然后把它的内部指针赋值给c_str_t.data。但按照Rust的所有权规则,函数返回时result会被自动销毁,对应的内存也会被释放,这就导致c_str_t.data变成了悬垂指针——指向已经被回收的内存。后续访问这个指针时,会触发未定义行为,你看到的首字符变成\u{0}就是这种行为的一种表现(也可能出现程序崩溃、数据乱码等其他诡异结果)。

另外还有个小细节:CString内部会自动在字符串末尾添加一个null终止符,但你的c_str_t.len是原Rust字符串的长度,这本身没问题,但悬垂指针才是核心问题。

修复方案

我们需要确保c_str_t.data指向的内存在使用期间保持有效,有两种常见的处理方式,分别适合不同的场景:

方案1:转移CString的所有权(纯Rust场景)

修改MyCStrExt的返回类型,把CString和c_str_t一起返回,这样CString的所有权会被带出函数,不会被提前销毁,指针也就始终有效:

#![allow(non_camel_case_types)]
use libc::{c_uchar, size_t};
use std::ffi::{CString, NulError};
use std::slice;

#[repr(C)]
pub struct c_str_t {
    pub len: size_t,
    pub data: *const c_uchar,
}

pub trait MyCStrExt<T> {
    // 修改返回类型,携带CString的所有权
    fn to_c_str(&self) -> Result<(CString, c_str_t), NulError>;
}

pub trait MyCStringExt {
    fn from_c_str_ref(nstr: &c_str_t) -> Option<String>;
}

impl<'a> MyCStrExt<&'a str> for str {
    fn to_c_str(&self) -> Result<(CString, c_str_t), NulError> {
        let c_string = CString::new(self)?;
        let c_str = c_str_t {
            data: c_string.as_ptr() as *const u8,
            len: self.len(),
        };
        Ok((c_string, c_str))
    }
}

impl MyCStringExt for String {
    fn from_c_str_ref(nstr: &c_str_t) -> Option<String> {
        unsafe {
            if nstr.data.is_null() {
                return None;
            }
            let slice = slice::from_raw_parts(nstr.data, nstr.len);
            String::from_utf8(slice.to_vec()).ok()
        }
    }
}

#[test]
fn test_to_c_str() {
    let s = "What does the fox say?";
    let (c_string, c_str) = s.to_c_str().unwrap();
    let round_trip = String::from_c_str_ref(&c_str);
    assert_eq!(round_trip, Some(s.to_string()));
    println!("{:?}", round_trip); // 输出: Some("What does the fox say?")
}

这种方式完全依赖Rust的所有权机制自动管理内存,不需要手动释放,适合纯Rust内部的字符串转换场景。

方案2:手动分配堆内存(FFI交互场景)

如果你的c_str_t需要传递给C代码,无法携带CString的所有权,那么可以手动在堆上复制字符串字节,让c_str_t.data指向这个独立的堆内存(注意:这种方式需要手动释放内存,避免泄漏):

#![allow(non_camel_case_types)]
use libc::{c_uchar, size_t};
use std::ffi::NulError;
use std::slice;
use std::alloc::{alloc, dealloc, Layout};

#[repr(C)]
pub struct c_str_t {
    pub len: size_t,
    pub data: *const c_uchar,
}

pub trait MyCStrExt<T> {
    fn to_c_str(&self) -> Result<c_str_t, NulError>;
}

pub trait MyCStringExt {
    fn from_c_str_ref(nstr: &c_str_t) -> Option<String>;
    // 添加手动释放内存的方法
    unsafe fn free_c_str(c_str: c_str_t);
}

impl<'a> MyCStrExt<&'a str> for str {
    fn to_c_str(&self) -> Result<c_str_t, NulError> {
        // 先验证字符串不含null字节(和CString的校验逻辑一致)
        if self.contains('\0') {
            return Err(NulError);
        }
        let layout = Layout::array::<u8>(self.len()).unwrap();
        // 分配堆内存
        let data = unsafe { alloc(layout) } as *mut c_uchar;
        if data.is_null() {
            std::alloc::handle_alloc_error(layout);
        }
        // 复制字符串字节到堆内存
        unsafe {
            std::ptr::copy_nonoverlapping(self.as_ptr(), data as *mut u8, self.len());
        }
        Ok(c_str_t {
            data,
            len: self.len(),
        })
    }
}

impl MyCStringExt for String {
    fn from_c_str_ref(nstr: &c_str_t) -> Option<String> {
        unsafe {
            if nstr.data.is_null() {
                return None;
            }
            let slice = slice::from_raw_parts(nstr.data, nstr.len);
            String::from_utf8(slice.to_vec()).ok()
        }
    }

    unsafe fn free_c_str(c_str: c_str_t) {
        if !c_str.data.is_null() {
            let layout = Layout::array::<u8>(c_str.len).unwrap();
            dealloc(c_str.data as *mut u8, layout);
        }
    }
}

#[test]
fn test_to_c_str() {
    let s = "What does the fox say?";
    let c_str = s.to_c_str().unwrap();
    let round_trip = String::from_c_str_ref(&c_str);
    assert_eq!(round_trip, Some(s.to_string()));
    println!("{:?}", round_trip);
    // 手动释放内存
    unsafe { String::free_c_str(c_str) };
}

这种方式的内存是独立分配的,C代码也可以通过标准的free函数释放(如果使用的是系统分配器),适合需要和C代码交互的场景。

额外注意点

  • 永远不要在Rust中保留悬垂指针,Rust的所有权机制就是用来避免这类内存安全问题的,一定要确保指针指向的内存在使用期间始终有效。
  • 你的c_str_t是带长度的结构(类似Pascal字符串),其实不需要null终止符,所以方案2里直接复制字节的方式比用CString更高效,也避免了额外的null字节开销。

内容的提问来源于stack exchange,提问作者Paul C

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 08:09:53