如何将 C 字符串转换为 Rust 字符串并通过 FFI 转换回来?

Posted

技术标签:

【中文标题】如何将 C 字符串转换为 Rust 字符串并通过 FFI 转换回来?【英文标题】:How do I convert a C string into a Rust string and back via FFI? 【发布时间】:2014-07-31 12:01:45 【问题描述】:

我正在尝试获取 C 库返回的 C 字符串,并通过 FFI 将其转换为 Rust 字符串。

mylib.c

const char* hello()
    return "Hello World!";

ma​​in.rs

#![feature(link_args)]

extern crate libc;
use libc::c_char;

#[link_args = "-L . -I . -lmylib"]
extern 
    fn hello() -> *c_char;


fn main() 
    //how do I get a str representation of hello() here?

【问题讨论】:

【参考方案1】:

在 Rust 中处理 C 字符串的最佳方式是使用来自 std::ffi 模块的结构,即 CStrCString

CStr 是动态大小的类型,因此只能通过指针使用。这使它与常规的str 类型非常相似。您可以使用不安全的CStr::from_ptr 静态方法从*const c_char 构造&CStr。这个方法是不安全的,因为不能保证你传递给它的原始指针是有效的,它确实指向一个有效的 C 字符串并且字符串的生命周期是正确的。

您可以使用&CStrto_str() 方法从&str 获取&str

这是一个例子:

extern crate libc;

use libc::c_char;
use std::ffi::CStr;
use std::str;

extern 
    fn hello() -> *const c_char;


fn main() 
    let c_buf: *const c_char = unsafe  hello() ;
    let c_str: &CStr = unsafe  CStr::from_ptr(c_buf) ;
    let str_slice: &str = c_str.to_str().unwrap();
    let str_buf: String = str_slice.to_owned();  // if necessary

您需要考虑*const c_char 指针的生命周期以及谁拥有它们。根据 C API,您可能需要对字符串调用特殊的释放函数。您需要仔细安排转换,以便切片不会超过指针。 CStr::from_ptr 返回具有任意生命周期的 &CStr 的事实在这里有所帮助(尽管它本身很危险);例如,您可以将 C 字符串封装到一个结构中并提供 Deref 转换,这样您就可以像使用字符串切片一样使用您的结构:

extern crate libc;

use libc::c_char;
use std::ops::Deref;
use std::ffi::CStr;

extern "C" 
    fn hello() -> *const c_char;
    fn goodbye(s: *const c_char);


struct Greeting 
    message: *const c_char,


impl Drop for Greeting 
    fn drop(&mut self) 
        unsafe 
            goodbye(self.message);
        
    


impl Greeting 
    fn new() -> Greeting 
        Greeting  message: unsafe  hello()  
    


impl Deref for Greeting 
    type Target = str;

    fn deref<'a>(&'a self) -> &'a str 
        let c_str = unsafe  CStr::from_ptr(self.message) ;
        c_str.to_str().unwrap()
    

此模块中还有另一种类型,称为CString。它与 CStr 的关系与 Stringstr 的关系相同 - CStringCStr 的拥有版本。这意味着它“持有”字节数据分配的句柄,删除CString 将释放它提供的内存(本质上,CString 包装了Vec&lt;u8&gt;,后者将被删除)。因此,当您想将 Rust 中分配的数据公开为 C 字符串时,它很有用。

不幸的是,C 字符串总是以零字节结尾,并且其中不能包含一个,而 Rust &amp;[u8]/Vec&lt;u8&gt; 正好相反——它们不以零字节结尾,并且可以包含任意数量的他们在里面。这意味着从 Vec&lt;u8&gt;CString 既不是无错误也不是无分配 - CString 构造函数都会检查您提供的数据中的零,如果找到则返回错误,并附加一个零字节到可能需要重新分配的字节向量的末尾。

String,它实现了Deref&lt;Target = str&gt;CString实现了Deref&lt;Target = CStr&gt;,所以你可以直接在CString上调用定义在CStr上的方法。这很重要,因为返回 C 互操作所需的 *const c_charas_ptr() 方法是在 CStr 上定义的。可以直接在CString的值上调用这个方法,很方便。

CString 可以从可以转换为Vec&lt;u8&gt; 的所有内容创建。 String&amp;strVec&lt;u8&gt;&amp;[u8] 是构造函数 CString::new() 的有效参数。当然,如果你传递一个字节切片或字符串切片,则会创建一个新的分配,而Vec&lt;u8&gt;String 将被消耗。

extern crate libc;

use libc::c_char;
use std::ffi::CString;

fn main() 
    let c_str_1 = CString::new("hello").unwrap(); // from a &str, creates a new allocation
    let c_str_2 = CString::new(b"world" as &[u8]).unwrap(); // from a &[u8], creates a new allocation
    let data: Vec<u8> = b"12345678".to_vec(); // from a Vec<u8>, consumes it
    let c_str_3 = CString::new(data).unwrap();

    // and now you can obtain a pointer to a valid zero-terminated string
    // make sure you don't use it after c_str_2 is dropped
    let c_ptr: *const c_char = c_str_2.as_ptr();

    // the following will print an error message because the source data
    // contains zero bytes
    let data: Vec<u8> = vec![1, 2, 3, 0, 4, 5, 0, 6];
    match CString::new(data) 
        Ok(c_str_4) => println!("Got a C string: :p", c_str_4.as_ptr()),
        Err(e) => println!("Error getting a C string: ", e),
      

如果您需要将CString 的所有权转移到C 代码,您可以调用CString::into_raw。然后你需要取回指针并在 Rust 中释放它; Rust 分配器不太可能与 mallocfree 使用的分配器相同。您需要做的就是调用CString::from_raw,然后让字符串正常删除。

【讨论】:

很好的答案,这对我帮助很大。与 c# 之类的 GC lang 接口时,cstr 生命周期中的不安全性是否仍然存在? @scape 是的,当然可以。我想说它在那里更重要,因为垃圾收集可能随时运行,特别是如果它是并发的。如果你不注意让 GC 端的字符串根植于某个地方,你可能会突然访问 Rust 端释放的一块内存。【参考方案2】:

除了@vladimir-matveev 所说的,你还可以在没有CStrCString 的帮助下在它们之间进行转换:

#![feature(link_args)]

extern crate libc;
use libc::c_char, puts, strlen;
use std::slice, str;

#[link_args = "-L . -I . -lmylib"]
extern "C" 
    fn hello() -> *const c_char;


fn main() 
    //converting a C string into a Rust string:
    let s = unsafe 
        let c_s = hello();
        str::from_utf8_unchecked(slice::from_raw_parts(c_s as *const u8, strlen(c_s)+1))
    ;
    println!("s == :?", s);
    //and back:
    unsafe 
        puts(s.as_ptr() as *const c_char);
    

只需确保在从 &str 转换为 C 字符串时,您的 &str 以 '\0' 结尾。 请注意,在上面的代码中,我使用strlen(c_s)+1 而不是strlen(c_s),所以s"Hello World!\0",而不仅仅是"Hello World!"。 (当然,在这种特殊情况下,它甚至可以仅使用 strlen(c_s)。但是使用新的 &str 不能保证生成的 C 字符串会在预期的位置终止。) 下面是运行代码的结果:

s == "Hello World!\u0"
Hello World!

【讨论】:

你可以在没有CStr的情况下转换from,但是没有理由避免它。您的转换返回是不正确,因为 Rust &amp;str 不是 NUL 终止的,因此不是有效的 C 字符串。 @Shepmaster,是的,Rust &str 通常不是以 NUL 结尾的,但由于它是由 C 字符串制成的,所以当您使用 s.as_ptr() 时它可以正常工作。为了更清楚,我现在将strlen(c_s) 更正为strlen(c_s)+1 那么现在您已经从标准库中复制了功能?请edit您的问题向未来的读者解释为什么他们应该选择这个解决方案而不是现有的答案。 这样做的一个原因是您在 no_std 环境中进行开发。

以上是关于如何将 C 字符串转换为 Rust 字符串并通过 FFI 转换回来?的主要内容,如果未能解决你的问题,请参考以下文章

8. 字符串转换整数 (atoi)(leetcode力扣算法 - java / rust)

8. 字符串转换整数 (atoi)(leetcode力扣算法 - java / rust)

如何将字符串与字符串文字匹配?

在 Rust 中将二进制字符串转换为带有前导零的十六进制字符串

将 c 样式字符串转换为 c++ 样式字符串

c语言中如何将字符串转化成整数型并输出?