从 String 到 *const i8 的正确方法是啥?

Posted

技术标签:

【中文标题】从 String 到 *const i8 的正确方法是啥?【英文标题】:What is the proper way to go from a String to a *const i8?从 String 到 *const i8 的正确方法是什么? 【发布时间】:2015-04-23 07:58:14 【问题描述】:

在我为Cassandra C++ driver 编写safe wrapper 的过程中,现在我的注意力转向了在调用具有以下签名的C 函数时避免内存泄漏:

cass_string_init2(const char* data, cass_size_t length);

cass_string_init(const char* null_terminated);

我尝试了几种名义上可行的不同方法,并产生了正确的结果,但我还没有找到一种方法来正确管理这些数据的生命周期。下面是两个示例方法。

pub fn str_to_ref(mystr:&str) -> *const i8 unsafe
    let cstr = CString::from_slice(mystr.as_bytes());
    cstr.as_slice().as_ptr()

pub fn str_to_ref(mystr: &str) -> *const i8 
    let l = mystr.as_bytes();
    unsafe 
        let b = alloc::heap::allocate(mystr.len()+1, 8);
        let s = slice::from_raw_parts_mut(b, mystr.len()+1);
        slice::bytes::copy_memory(s, l);
        s[mystr.len()] = 0;
        return b as *const i8;
    

第一个执行无效的内存访问,例如

==26355==  Address 0x782d140 is 0 bytes inside a block of size 320 free'd
==26355==    at 0x1361A8: je_valgrind_freelike_block (in /home/tupshin/workspaces/rust/cql-ffi/target/basic)
==26355==    by 0x11272D: heap::imp::deallocate::h7b540039fbffea4dPha (in /home/tupshin/workspaces/rust/cql-ffi/target/basic)
==26355==    by 0x112679: heap::deallocate::h3897fed87b942253tba (in /home/tupshin/workspaces/rust/cql-ffi/target/basic)
==26355==    by 0x112627: vec::dealloc::h7978768019700822177 (in /home/tupshin/workspaces/rust/cql-ffi/target/basic)
==26355==    by 0x112074: vec::Vec$LT$T$GT$.Drop::drop::h239007174869221309 (in /home/tupshin/workspaces/rust/cql-ffi/target/basic)
==26355==    by 0x111F9D: collections..vec..Vec$LT$i8$GT$::glue_drop.5732::h978a83960ecb86a4 (in /home/tupshin/workspaces/rust/cql-ffi/target/basic)
==26355==    by 0x111F6D: std..ffi..c_str..CString::glue_drop.5729::h953a595760f34a9d (in /home/tupshin/workspaces/rust/cql-ffi/target/basic)
==26355==    by 0x112903: cql_ffi::helpers::str_to_ref::hef3994fa55168b90bqd (in /home/tupshin/workspaces/rust/cql-ffi/target/basic)
=

而第二个不知道何时释放其内存,导致:

==29782== 8 bytes in 1 blocks are definitely lost in loss record 1 of 115
==29782==    at 0x12A5B2: je_mallocx (in /home/tupshin/workspaces/rust/cql-ffi/target/basic)
==29782==    by 0x1142D5: heap::imp::allocate::h3fa8a1c097e9ea53Tfa (in /home/tupshin/workspaces/rust/cql-ffi/target/basic)
==29782==    by 0x114221: heap::allocate::h18d191ce51ab2236gaa (in /home/tupshin/workspaces/rust/cql-ffi/target/basic)
==29782==    by 0x112874: cql_ffi::helpers::str_to_ref::h5b60f207d1e31841bqd (helpers.rs:25)

使用这两种方法中的任何一种作为起点,或者完全不同的方法,我真的很感激一些关于正确方法的指导。

编辑:

Shep 的回答 完美 使用 cass_string_init 和 cass_string_init2 解决了我的问题。太感谢了。但是,我仍然不清楚将 *const i8 参数传递给其他函数,例如:

CASS_EXPORT CassError
cass_cluster_set_contact_points(CassCluster* cluster,
const char* contact_points);

它期望被传递一个对空终止字符串的引用。

基于之前适用于 CassStrings 的方法以及 CString 文档,我提出了以下建议:

pub struct ContactPoints(*const c_char);

pub trait AsContactPoints 
    fn as_contact_points(&self) -> ContactPoints;


impl AsContactPoints for str 
    fn as_contact_points(&self) -> ContactPoints 
        let cstr = CString::new(self).unwrap();
        let bytes = cstr.as_bytes_with_nul();
        let ptr = bytes.as_ptr();
        ContactPoints(ptr as *const i8)
    

(过多的 let 绑定只是为了确保我没有遗漏任何细微之处)

运行正常,但 valgrind 抱怨:

==22043== Invalid read of size 1
==22043==    at 0x4C2E0E2: strlen (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==22043==    by 0x4F8AED8: cass_cluster_set_contact_points (in /usr/local/lib/libcassandra.so.1.0.0)
==22043==    by 0x11367A: cql_ffi::cluster::CassCluster::set_contact_points::h575496cbf7644b9e6oa (cluster.rs:76)

【问题讨论】:

Shep,再次感谢您的帮助。 :) 我想我几乎已经完成了完整驱动程序 api 的不安全部分。不过,我还缺少什么? (见编辑) 我已经更新了我的答案。以后请务必在 cmets 中使用 @-mention,否则人们不会看到您在与他们交谈。刚好重读了这一篇。 :-) 你是Rustaceans中的神。 【参考方案1】:

Cassandra C API for cass_string_init2 看起来像:

注意:这不分配内存。该对象包装了传递给该函数的指针。

CASS_EXPORT CassString
cass_string_init2(const char* data, cass_size_t length);

也就是说,它接受一个字符串,并返回一个字符串的替代表示。那representation looks like:

typedef struct CassString_ 
    const char* data;
    cass_size_t length;
 CassString;

这是你想在 Rust 代码中使用 #[repr(C)] 的地方:

#[repr(C)]
struct CassStr 
    data: *const c_char,
    length: size_t,

你能做的最好的事情就是让字符串自动转换成这个结构:

trait AsCassStr 
    fn as_cass_str(&self) -> CassStr;


impl AsCassStr for str 
    fn as_cass_str(&self) -> CassStr 
        CassStr 
            data: self.as_bytes(),
            length: self.len(),
        
    

然后让您的 API 接受任何实现 AsCassStr 的内容。这也允许您拥有自己的变体。您可能还想查看 PhantomData 以允许强制执行 CassStr 对象的生命周期。

注意通常您希望使用CString 来避免带有内部 NUL 字节的字符串。但是,由于 API 接受长度参数,因此它可能原生支持它们。您需要进行实验才能找到答案。如果没有,那么您需要使用CString,如下所示。

问题的后半部分

让我们逐行看看你的函数:

impl AsContactPoints for str 
    fn as_contact_points(&self) -> ContactPoints 
        let cstr = CString::new(self).unwrap(); // 1
        let bytes = cstr.as_bytes_with_nul();   // 2
        let ptr = bytes.as_ptr();               // 3
        ContactPoints(ptr as *const i8)         // 4
                                               // 5

    我们创建一个新的CString。这会在某处分配一点内存,验证字符串没有内部 NUL 字节,然后逐字节复制我们的字符串,并添加一个尾随零。 我们得到一个切片,它引用我们在第 1 步中复制和验证的字节。回想一下,切片是一个指向数据的指针加上一个长度。 我们将切片转换为指针,忽略长度。 我们将指针存储在一个结构中,并使用它作为我们的返回值 函数退出,所有局部变量都被释放。请注意,cstr 是一个局部变量,因此它所保存的字节也同样被释放。你现在有一个悬空指针。不好!

您需要确保CString 的使用寿命与需要一样长。希望您调用的函数不会保留对它的引用,但是没有简单的方法可以从函数签名中分辨出来。你可能想要这样的代码:

fn my_cass_call(s: &str) 
    let s = CString::new(s).unwrap();
    cass_call(s.as_ptr()) // `s` is still alive here

这里的好处是您永远不会将指针存储在您自己的变量中。请记住,原始指针没有生命周期,因此您必须非常小心它们!

【讨论】:

以上是关于从 String 到 *const i8 的正确方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

没有合适的从“const std :: string”到“time_t”的转换函数

从 bool (i1) 向量到 i8、i16 等的 LLVM 位转换是不是定义明确?

如何正确声明从非常量迭代器到指针的 const 指针

不存在从“const std::string”到“char *”的合适转换函数?

VS2017中遇到不存在从string到const char*的转换函数的解决方法

将字符串 (const char*) 从 C++ 传递到 C# 时,SWIG_csharp_string_callback 会导致内存泄漏