将路径转换为*c_char的最直接方法是什么?

26

对于给定的std::path::Path,最直接的方式将其转换为以空字符结尾的std::os::raw::c_char是什么?(用于传递给需要路径的C函数)。

use std::ffi::CString;
use std::os::raw::c_char;
use std::os::raw::c_void;

extern "C" {
    some_c_function(path: *const c_char);
}

fn example_c_wrapper(path: std::path::Path) {
    let path_str_c = CString::new(path.as_os_str().to_str().unwrap()).unwrap();

    some_c_function(path_str_c.as_ptr());
}
有没有办法避免出现那么多中间步骤呢?
Path -> OsStr -> &str -> CString -> as_ptr()

1
假设Path可以转换为C字符串是不准确的。平台可以使用不同的编码方式,这就是为什么首先存在这些抽象的原因。如果您限制在类UNIX系统上,则可以使用OsStrExt - Shepmaster
2
请注意,您还要转换为String,它必须是UTF-8格式的,尽管C字符串不需要这样做。 - Shepmaster
4个回答

15
这并不像看起来的那么简单。你没有提供一个信息:C函数期望路径使用哪种编码?
在Linux上,路径“只是”字节数组(0无效),应用程序通常不会尝试解码它们。(但是,它们可能需要使用特定编码对其进行解码,例如将其显示给用户,此时它们通常会根据当前语言环境尝试按照UTF-8编码进行解码。)
在Windows上,情况更加复杂,因为有使用“ANSI”代码页和使用“Unicode”(UTF-16)的API函数的变体。此外,Windows不支持将UTF-8设置为“ANSI”代码页。这意味着,除非库明确期望UTF-8并将路径转换为本地编码,否则传递UTF-8编码的路径肯定是错误的(尽管对于仅包含ASCII字符的字符串,它可能会“看起来”工作)。
(我不知道其他平台,但已经够乱的了。)
在 Rust 中,Path 只是 OsStr 的一个包装器。 OsStr 使用平台相关的表示方式,当字符串确实是有效的 UTF-8 时,它恰好与 UTF-8 兼容,但非 UTF-8 字符串使用未指定的编码(在 Windows 上,它实际上使用 WTF-8,但这不是合同;在 Linux 上,它只是字节数组)。
在将路径传递给C函数之前,您必须确定它期望字符串的编码方式,如果与Rust的编码不匹配,则必须在将其包装在CString中之前进行转换。在Unix-based目标上,OsStrExt trait可用,并提供对OsStr作为字节片的访问。
在平台无关的方式中,Rust不允许您将PathOsStr转换为除str以外的任何内容。 Rust曾经在OsStr上提供了一个to_cstring方法,但它从未稳定下来,并且在Rust 1.6.0中被弃用,因为人们意识到该行为对于Windows不合适(它返回UTF-8编码的路径,但Windows API不支持!)。

3
Locale在Linux上可能是一个系统猜测,但它与路径编码实际上并没有太大关系。路径可以是任意字节,除了0以外。 - bluss

8
作为一个仅是围绕 OsStr 的轻包装器,路径(Path)可以直接传递给您的 C 函数。但要成为有效的 C 字符串,我们必须添加 NUL 终止符。因此,我们必须分配 CString。
另一方面,转换为 str 既存在风险(如果 Path 不是有效的 UTF-8 字符串怎么办?),也是不必要的开销:我使用 as_bytes() 而不是 to_str()。
fn example_c_wrapper<P: AsRef<std::path::Path>>(path: P) {
    let path_str_c = CString::new(path.as_ref().as_os_str().as_bytes()).unwrap();

    some_c_function(path_str_c.as_ptr());
}

这是针对Unix系统的。我不知道在Windows系统下是如何工作的。


6

如果你的目标是将路径转换为在编译代码的任何平台上都被解释为 "本机" 路径的一些 字节序列,那么最直接的方法是使用每个想要支持的平台的OsStrExt

let path = ..;
let mut buf = Vec::new();

#[cfg(unix)] {
    use std::os::unix::ffi::OsStrExt;
    buf.extend(path.as_os_str().as_bytes());
    buf.push(0);
}

#[cfg(windows)] {
    use std::os::windows::ffi::OsStrExt;
    buf.extend(path.as_os_str()
               .encode_wide()
               .chain(Some(0))
               .map(|b| {
                   let b = b.to_ne_bytes();
                   b.get(0).map(|s| *s).into_iter().chain(b.get(1).map(|s| *s))
               })
               .flatten());
}

这段代码[1]在Linux上运行时,会给您提供一个字节缓冲区,该缓冲区表示路径为一系列以null结尾的字节,并且在Windows上运行时表示“unicode”(utf16)。在其他平台上,您可以添加将OsStr转换为str的备选方案,但我强烈建议不要这样做。(见下文)

对于Windows,在将其用于Windows上的Unicode功能(例如_wfopen)之前,您需要将缓冲区指针强制转换为wchar_t*。此代码假定wchar_t大小为两个字节,并且缓冲区已正确地对齐到wchar_t

在Linux侧,只需使用指针本身即可。

关于将路径转换为Unicode字符串: 不要。与此处和其他地方的推荐相反,盲目将路径转换为UTF8不是处理系统路径的正确方法。要求路径有效的Unicode将在遇到无效Unicode路径时导致代码失败(而不是遇到)。如果您正在处理现实世界的路径,则无疑会处理非UTF8路径。第一次就做对将有助于长期避免很多痛苦和困苦。

[1]:这段代码直接摘自我正在开发的库(请随意重用)。它已通过Wine对Linux和64位Windows进行了测试。


2
如果您想生成一个Vec<u8>,我通常会这样做:

最初的回答
#[cfg(unix)]
fn path_to_bytes<P: AsRef<Path>>(path: P) -> Vec<u8> {
    use std::os::unix::ffi::OsStrExt;
    path.as_ref().as_os_str().as_bytes().to_vec()
}

#[cfg(not(unix))]
fn path_to_bytes<P: AsRef<Path>>(path: P) -> Vec<u8> {
    // On Windows, could use std::os::windows::ffi::OsStrExt to encode_wide(),
    // but you end up with a Vec<u16> instead of a Vec<u8>, so that doesn't
    // really help.
    path.as_ref().to_string_lossy().to_string().into_bytes()
}

请注意,非UNIX上的非UTF8路径将无法正确支持。 如果使用Thrift /协议缓冲区而不是C API,则可能需要Vec<u8>

最初的回答:

在非UNIX系统上,非UTF8路径将无法正确支持。请注意,如果使用Thrift/协议缓冲区而不是C API,您可能需要使用Vec<u8>


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