【问题标题】:How do I handle errors from libc functions in an idiomatic Rust manner?如何以惯用的 Rust 方式处理来自 libc 函数的错误?
【发布时间】:2017-03-13 19:55:07
【问题描述】:

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);
}

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

如果fork() 返回&lt; 0,有没有办法返回Err

我发现了两件事很接近:

  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")。这现在只有一行,但它panics 而不是返回Err。我想这可以使用ok_or 解决。

libc 的一些函数使用&lt; 0 作为标记(例如fork),而另一些函数使用&gt; 0(例如pthread_attr_init),所以这需要另一个参数。

有什么办法可以解决这个问题吗?

【问题讨论】:

    标签: error-handling rust libc


    【解决方案1】:

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

    返回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)
    }
    

    包装 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
        ...
    }
    

    限制返回类型

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

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

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

    使用选项和其他枚举

    Rust 有一个丰富的类型系统,它允许表达必须在 C 中编码为魔法值的事物。回到 fork() 示例,该函数返回 0 表示子返回。这将自然地用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
    }
    

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

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

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

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

    当返回系统资源的句柄(例如文件描述符或 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
    

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

    【讨论】:

    • 关于处理 libc 的一般返回类型的精彩综述!这将是一个很好的文档帖子
    【解决方案2】:

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

    pub fn fork() -> Result<ForkResult>
    

    然后只需 use normal error handling 就像 try!?


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

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

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

    所以这需要另一个参数

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

    【讨论】:

    • 当然,fork 只是一个假例子。我目前遇到的问题是pthread_attr_init,目前nix 似乎还不支持它
    • @hansaplast 那么似乎正确的解决方案是向 nix 提交 PR 并添加它。然后,您只需使用他们已经建立的任何机制将错误代码转换为优雅的类型。
    • @hansaplast 请记住,您还可以在 PR 审核期间临时 fork nix 以使用您的本地更改。这样你就不会等待。
    • 谢谢,我有一个特殊情况,我将一本书的 C 代码移植到 Rust 并希望尽可能接近系统调用以用于教育目的,所以我想我会去我将使用每个哨兵的方法编写特征的方式
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-09-11
    • 2021-10-10
    • 1970-01-01
    • 1970-01-01
    • 2021-11-02
    • 1970-01-01
    • 2013-06-15
    相关资源
    最近更新 更多