将一个 &str 复制到 Rust 中的数组指针中。

4
我有一个 FFI 函数,它分配并返回一个指向已知长度数组的 *mut::std::os::raw::c_char。我还有一个 Rust 字符串 s:&str。我只想将 s 的内容复制到字符指针中添加空字节,就像 C 中的 strcpy() 一样,但我找不到用 Rust 实现这个操作的简洁方法。如果可能的话,我希望在只进行一次拷贝的情况下完成操作,而不需要中间分配,但如果这不可能,那也没关系。
以下是我的变量:
let ptr: *mut ::std::os::raw::c_char;
let s: &str;

我已经考虑过:

  • std::ffi::CStr::from_ptr 用于创建表示分配内存的 CStr,但这似乎是不可变的
  • 使用 CString,但这似乎是为 Rust "拥有" 的字符串而设计的,所以我不确定它是否适用于这里,当 Rust 不负责释放此数组时
  • 我可以使用 std::slice::from_raw_parts_mutptr 创建一个可变切片,但这有点低级,我希望有一种更高级的字符串操作选项,可以为我添加 null,而不是让我自己添加

有没有一种优雅的方法来解决这个问题?


2
你似乎完全误解了整件事情(这让我非常困惑)。没有理由将 ptr 转换为字符串,你应该将字符串转换为指针,从中使用 std::ptr::copy 复制到指针,然后显式地以空字符结尾。你必须手动终止,因为 &str 不是以空字符结尾的,并且强制进行空字符结尾需要原地突变(无法在 &str 上完成)或通过 CString::new 进行分配。 - Masklinn
当然,那听起来是一个合理的解决方案,你可以自由地将其写成答案。我认为把我的失败尝试称为“错误的方法”是不公平的,因为它们并不是问题本身的一部分。我只是想指出,将其视为字符串复制而不是字节复制操作可能会通过自动添加空字节使其更简单,但如果这不是最佳方法,那也完全没关系。 - Migwell
copy_from_slice是一个完整指针复制的替代方法,但这需要从ptr创建一个切片,而这又要求ptr的目标全部初始化,否则你必须使用MaybeUninit.write_slice,而这只在夜间版中可用。 - Masklinn
使用 std::ffi::CStr::from_ptr,然后使用 to_bytes_mut 使其可变,接着使用 copy_to_nonoverlapping 将字符串复制到其中。在字节切片的末尾手动添加一个空字节。对于简单的任务来说,这听起来像是不必要的转换。 - Jishan Shaikh
是的,整个数组都被分配了。如果使用切片更整洁,那我会这样做,尽管这不是我在第三个点中建议的吗? - Migwell
显示剩余4条评论
2个回答

2

嗯,它需要使用unsafe,因为你要写入一些未知的指针。

下面的代码假定C使用UTF-8编码。

请注意,我故意使用memcpy而不是从外部指针创建切片,因为不能保证该指针指向的内存已初始化,从未初始化的内存创建切片是未定义的行为。

#![forbid(unsafe_op_in_unsafe_fn)]

use std::os::raw::c_char;

/// # Safety
/// `arr` must point to some valid memory which doesn't overlap with `s`.
/// `arr` must point to at least `known_len` bytes.
unsafe fn copy_rust_str_to_c_arr(s: &str, arr: *mut c_char, known_len: usize) {
    // Note, that panics must not be running across FFI boundaries
    // because it would be undefined behaviour.

    // But writing more bytes than allocated is also UB.
    assert!(s.len() + 1 <= known_len, "Not enough memory to copy str");
    // This is true for any modern architecture.
    assert_eq!(std::mem::size_of::<c_char>(), 1);
    unsafe {
        let our_ptr = s.as_bytes().as_ptr();
        // Safety: our_ptr is valid because it produced from string reference.
        // `arr` must be valid by safety requirements of function.
        // We checked length right before.
        std::ptr::copy_nonoverlapping(our_ptr, arr.cast(), s.len());
        // Set zero terminator for C.
        *arr.add(s.len()) = b'\0' as _;
    }
}

#[test]
fn test_copy() {
    use std::ffi::{CStr, CString};
    let s = "Hello, World!";
    // This would be used as a pointer from C.
    let mut dest: Vec<c_char> = vec!['\t' as _; s.len() * 2];
    unsafe {
        copy_rust_str_to_c_arr(s, dest.as_mut_ptr(), dest.len());
    }
    let dest = unsafe { CStr::from_ptr(dest.as_ptr()) };
    let expected = CString::new(s).unwrap();
    assert_eq!(&*expected , dest);
}

此外,夜间版本编译出的机器码与其他版本基本相同。 即使指针数据未初始化,也可以创建 MaybeUninit 的切片。

#![feature(maybe_uninit_write_slice)]
#![forbid(unsafe_op_in_unsafe_fn)]

use core::mem::MaybeUninit;
use std::os::raw::c_char;

/// # Safety
/// `arr` must point to some valid memory which doesn't overlap with `s`.
/// `arr` must point to at least `known_len` bytes.
unsafe fn copy_rust_str_to_c_arr(s: &str, arr: *mut c_char, known_len: usize) {
    // Note, that panics must not be running across FFI boundaries
    // because it would be undefined behaviour.

    // This is true for any modern architecture.
    assert_eq!(std::mem::size_of::<c_char>(), 1);
    let target: &mut [MaybeUninit<u8>] =
        unsafe { std::slice::from_raw_parts_mut(arr.cast(), known_len) };
    MaybeUninit::write_slice(&mut target[..s.len()], s.as_bytes());
    // Set zero terminator for C.
    target[s.len()] = MaybeUninit::new(b'\0');
}

0
最终我采用了这种方法,尽早将char*转换为&[u8],使我能够使用切片而不是指针算术来进行复制。虽然我原本希望避免这种情况(如原问题所述),但我发现使用切片至少比使用指针更高级。
// Convert the pointer into a slice
let dst_slice: &mut [u8];
unsafe {
    dst_slice = std::slice::from_raw_parts_mut(
        ptr as *mut u8,
        len
    );
}
// Copy the string
dst_slice[0..(len-1)].copy_from_slice(s.as_bytes());
// Add the null terminator
dst_slice[len - 1] = 0;

游乐场链接:https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=c1c3554114ec9806d07cf8c62a7b6171


只有在输出字符串被初始化的情况下,这才是有效的。 - Chayim Friedman
如果输出指针指向未初始化的数据,这将导致未定义的行为,因此我不建议在抽象情况下编写这样的代码,例如当您不知道目标指针来自哪里时。 - Angelicos Phosphoros
@AngelicosPhosphoros 你们俩能详细解释一下吗?如果我要写入的目标已经初始化了内存,这有什么关系呢?我不是从中读取,只是写入。 - Migwell
因为即使构造对未初始化数据的引用也是无效操作。这不管你是否与其交互或如何交互都不重要。您可以在此处阅读更多内容https://doc.rust-lang.org/nomicon/。 - Angelicos Phosphoros
您还可以阅读 from_raw_parts_mut 的文档:https://doc.rust-lang.org/beta/std/slice/fn.from_raw_parts_mut.html。它指出:“数据必须对**读取和写入都有效**”。 - Angelicos Phosphoros
是的,但正如 Rust 文档中所定义的那样,“有效”并不意味着“初始化”。它只是指可以合理写入的已分配内存:https://doc.rust-lang.org/beta/std/ptr/index.html#safety - Migwell

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