Rust: 如何使用 ffi 将 &cstr 转换为 String,然后再转换回去?

105
我正在尝试通过 FFI 将由 C 库返回的 C 字符串(&cstr)转换为 Rust 字符串。

mylib.c

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

main.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?
}

我将更新问题以匹配所选答案。 - undefined
2个回答

163
使用std::ffi模块中的结构体CStrCString是在Rust中处理C字符串的最佳方法。 CStr是一种动态大小类型,因此只能通过指针使用。这使它与常规的str类型非常相似。您可以使用不安全的CStr::from_ptr静态方法从*const c_char构造&CStr。这个方法是不安全的,因为无法保证您传递给它的原始指针是有效的,它确实指向一个有效的C字符串,并且该字符串的生命周期是正确的。
你可以通过 &CStrto_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<u8>,而后者将被删除)。因此,当您想要将在Rust中分配的数据公开为C字符串时,它非常有用。

抱歉,C字符串总是以零字节结尾,不能在其内部包含一个零字节,而Rust的&[u8]/Vec<u8>恰好相反 - 它们不以零字节结尾,并且可以包含任意数量的零字节。这意味着从Vec<u8>转换为CString既不是无错误的,也不是无需分配内存的——CString构造函数会检查您提供的数据中是否存在零字节,如果发现,则返回错误,并将零字节附加到字节向量的末尾,这可能需要重新分配其内存。

String 类似,它实现了 Deref<Target = str>CString 实现了 Deref<Target = CStr>,因此您可以在 CString 上直接调用在 CStr 上定义的方法。这很重要,因为返回用于 C 交互的 *const c_charas_ptr() 方法是在 CStr 上定义的。您可以直接在 CString 值上调用此方法,这非常方便。

CString 可以从任何可以转换为 Vec<u8> 的内容创建。 String&strVec<u8>&[u8] 都是构造函数 CString::new() 的有效参数。当然,如果传递一个字节切片或字符串切片,则会创建一个新的分配,而 Vec<u8>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语言进行接口时,cstr生命周期中的不安全性是否仍然存在? - scape
@scape 当然,它确实很重要。我会说在这里甚至更加重要,因为垃圾回收可能随时运行,特别是如果它是并发的。如果您不注意将字符串保留在GC侧的某个根位置,您可能会突然在Rust侧访问已释放的内存块。 - Vladimir Matveev
这并没有解决从CString -> String,或者从CString -> &str的问题,尽管这才是问题的关键所在。 - undefined

7

除了@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!\u{0}"
Hello World!

1
你可以在没有使用CStr的情况下进行from转换,但避免使用它是没有理由的。你的转换回来是不正确的,因为Rust的&str没有以NUL结尾,因此不是有效的C字符串。 - Shepmaster
@Shepmaster,是的,Rust的&str通常不是以NUL结尾的,但由于这个字符串是从C字符串创建的,所以当你执行s.as_ptr()时它可以正常工作。为了更清晰,我现在已经将strlen(c_s)更正为strlen(c_s)+1 - Des Nerger
1
那么现在你已经复制了标准库的功能?请[编辑]您的问题,向未来的读者解释为什么他们应该选择这个解决方案而不是现有的答案。 - Shepmaster
4
做这样的事情的一个原因是你在一个没有标准库(no_std)的环境中进行开发。 - Myk Melez
1
如果您需要在no_std环境中使用CStr,则https://github.com/Amanieu/cstr_core crate是一个不错的选择。唯一的缺点是它依赖于cty,而cty有一个开放的合并请求来修复AVR支持。 - Mutant Bob
这是将C字符串转换为RUST字符串的最简单解决方案。 - Sunding Wei

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接