如何以惯用的 Rust 方式处理来自 libc 函数的错误?

How do I handle errors from libc functions in an idiomatic Rust manner?

libc 的错误处理通常是 return 某些东西 < 0 以防出现错误。我发现自己一遍又一遍地这样做:

let pid = fork()
if pid < 0 {
    // Please disregard the fact that `Err(pid)`
    // should be a `&str` or an enum
    return Err(pid);
}

我觉得这需要 3 行错误处理很难看,尤其是考虑到这些测试在这种代码中非常频繁。

有没有办法 return 一个 Err 以防 fork() returns < 0

我发现两件事很接近:

  1. assert_eq!。这需要另一行,它 panics 因此调用者无法处理错误。
  2. 使用如下特征:

    pub trait LibcResult<T> {
        fn to_option(&self) -> Option<T>;
    }
    
    impl LibcResult<i64> for i32 {
        fn to_option(&self) -> Option<i64> {
            if *self < 0 { None } else { Some(*self) }
        }
    }
    

我可以写 fork().to_option().expect("could not fork")。现在只有一行,但它 panic 而不是 return 一个 Err。我想这可以使用 ok_or.

来解决

libc 的某些函数将 < 0 作为标记(例如 fork), while others use > 0 (e.g. pthread_attr_init),因此这需要另一个参数。

是否有解决此问题的方法?

best 选项是不重新实现 Universe。相反,使用 nix,它为您包装了所有内容,并完成了转换所有错误类型和处理标记值的艰苦工作:

pub fn fork() -> Result<ForkResult>

然后use normal error handling喜欢try!?


当然,您可以通过将特征转换为返回 Results 并包括特定的错误代码然后使用 try!? 来重写所有 nix,但为什么会你?

Rust 中没有什么神奇的东西可以为您将负数或正数转换为域特定的错误类型。您已经拥有的代码是正确的方法,一旦您通过直接创建它或通过 ok_or 之类的东西来增强它以使用 Result

一个中间解决方案是重用 nix 的 Errno 结构,也许在上面加上你自己的特征糖。

so this would need another argument

我认为采用不同的方法会更好:一种用于负标记值,一种用于正标记值。

如其他答案所示,尽可能使用 pre-made 包装器。如果不存在此类包装器,以下准则可能会有所帮助。

Return Result 表示错误

包含错误信息的惯用 Rust return 类型是 Resultstd::result::Result). For most functions from POSIX libc, the specialized type std::io::Result is a perfect fit because it uses std::io::Error 来编码错误,它包括所有由 errno 值表示的标准系统错误。避免重复的一个好方法是使用效用函数,例如:

use std::io::{Result, Error};

fn check_err<T: Ord + Default>(num: T) -> Result<T> {
    if num < T::default() {
        return Err(Error::last_os_error());
    }
    Ok(num)
}

包装 fork() 看起来像这样:

pub fn fork() -> Result<u32> {
    check_err(unsafe { libc::fork() }).map(|pid| pid as u32)
}

Result 的使用允许惯用用法,例如:

let pid = fork()?;  // ? means return if Err, unwrap if Ok
if pid == 0 {
    // child
    ...
}

限制 return 类型

如果修改 return 类型以便仅包含“可能的”值,该函数将更易于使用。例如,如果一个函数在逻辑上没有 return 值,但是 returns 一个 int 只是为了传达错误的存在,Rust 包装器应该 return 什么都没有:

pub fn dup2(oldfd: i32, newfd: i32) -> Result<()> {
    check_err(unsafe { libc::dup2(oldfd, newfd) })?;
    Ok(())
}

另一个例子是逻辑上 return 无符号整数的函数,例如 PID 或文件描述符,但仍将其结果声明为带符号的以包含 -1 错误 return 值。在这种情况下,请考虑 return 在 Rust 中使用无符号值,如上面的 fork() 示例所示。 nix 通过使 fork() return Result<ForkResult> 更进一步,其中 ForkResult 是一个真正的枚举,具有 is_child() 等方法,并且来自其中 PID 是使用模式匹配提取的。

使用选项和其他枚举

Rust 有一个丰富的类型系统,允许表达必须在 C 中编码为魔法值的东西。return 到 fork() 的例子,那个函数 returns 0来表示 child return。这将自然地用 Option 表示,并且可以与上面显示的 Result 组合:

pub fn fork() -> Result<Option<u32>> {
    let pid = check_err(unsafe { libc::fork() })? as u32;
    if pid != 0 {
        Some(pid)
    } else {
        None
    }
}

这个API的用户将不再需要与魔法值进行比较,而是使用模式匹配,例如:

if let Some(child_pid) = fork()? {
    // execute parent code
} else {
    // execute child code
}

Return 值而不是使用输出参数

C 通常 returns 使用 输出参数 的值,结果存储到的指针参数。这要么是因为实际的 return 值是为错误指示器保留的,要么是因为需要对多个值进行 returned,而 returning 结构在历史 C 编译器中的支持很差.

相比之下,Rust 的 Result 支持独立于错误信息的 return 值,并且对 returning 多个值没有任何问题。多个值 return 作为元组编辑比输出参数更符合人体工程学,因为它们可以在表达式中使用或使用模式匹配捕获。

将系统资源包装在拥有的 objects

当 return 处理系统资源时,例如文件描述符或 Windows 句柄,最好将它们 return 包装在 object 中,实现 Drop 释放他们。这将使包装器的用户不太可能犯错,并且它使 return 值的使用更加惯用,消除了对 close() 的笨拙调用和失败导致的资源泄漏的需要这样做。

pipe()为例:

use std::fs::File;
use std::os::unix::io::FromRawFd;

pub fn pipe() -> Result<(File, File)> {
    let mut fds = [0 as libc::c_int; 2];
    check_err(unsafe { libc::pipe(fds.as_mut_ptr()) })?;
    Ok(unsafe { (File::from_raw_fd(fds[0]), File::from_raw_fd(fds[1])) })
}

// Usage:
// let (r, w) = pipe()?;
// ... use R and W as normal File object

pipe() 包装器 return 有多个值并使用包装器 object 来引用系统资源。此外,它 returns File objects 在 Rust 标准库中定义并被 Rust 的 IO 层接受。