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

han*_*ast 5 error-handling libc rust

libc 的错误处理通常是< 0在发生错误时返回一些内容。我发现自己一遍又一遍地这样做:

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

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

有没有办法返回一个Err以防万一fork()返回< 0

我发现两件事很接近:

  1. assert_eq!. 这需要另一行,panic因此调用者无法处理错误。
  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) }
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

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

libc 的某些函数具有< 0标记(例如fork),而其他函数则使用> 0(例如pthread_attr_init),因此这需要另一个参数。

有什么东西可以解决这个问题吗?

use*_*342 8

如另一个答案所示,尽可能使用预制包装器。如果此类包装器不存在,以下指南可能会有所帮助。

返回Result指示错误

包含错误信息的惯用 Rust 返回类型是Result( std::result::Result)。对于来自 POSIX libc 的大多数函数,专用类型std::io::Result非常适合,因为它用于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)
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

限制返回类型

如果修改返回类型以便只包含“可能的”值,则该函数将更易于使用。例如,如果一个函数在逻辑上没有返回值,但返回 anint只是为了传达错误的存在,那么 Rust 包装器应该不返回任何内容:

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

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

使用选项和其他枚举

Rust 有一个丰富的类型系统,它允许表达必须在 C 中编码为魔术值的东西。回到这个fork()例子,该函数返回 0 以指示子返回。这将自然地用 an 表示,Option并且可以与Result上面显示的结合起来:

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

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

if let Some(child_pid) = fork()? {
    // execute parent code
} else {
    // execute child code
}
Run Code Online (Sandbox Code Playgroud)

返回值而不是使用输出参数

C 经常使用输出参数返回值,即存储结果的指针参数。这要么是因为实际返回值是为错误指示符保留的,要么是因为需要返回多个值,而历史 C 编译器对返回结构的支持很差。

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

将系统资源包装在拥有的对象中

当返回系统资源的句柄时,例如文件描述符或 Windows 句柄,最好将它们包装在实现Drop释放它们的对象中返回。这将使包装器的用户不太可能犯错误,并且它使返回值的使用更加惯用,消除了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
Run Code Online (Sandbox Code Playgroud)

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


She*_*ter 5

最好选择是不重新实现宇宙。相反,请使用nix,它为您包装了所有内容,并完成了转换所有错误类型和处理哨兵值的艰苦工作:

pub fn fork() -> Result<ForkResult>
Run Code Online (Sandbox Code Playgroud)

然后只需使用正常的错误处理,try!?


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

Rust 中没有什么神奇的功能可以将负数或正数转换为特定于域的错误类型。您已经拥有的代码是正确的方法,一旦您Result通过直接创建或通过类似ok_or.

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

所以这需要另一个论点

我想说最好有不同的方法:一种用于负哨兵值,一种用于正哨兵值。

  • 谢谢,我有一个特殊的情况,我将一本书的 C 代码移植到 Rust,并希望出于教育目的尽可能接近系统调用,所以我想我会采用编写特征的方式每个哨兵有一个方法 (2认同)