Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

libc's error handling is usually to return something < 0 in case of an error. I find myself doing this over and over:

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

I find it ugly that this needs 3 lines of error handling, especially considering that these tests are quite frequent in this kind of code.

Is there a way to return an Err in case fork() returns < 0?

I found two things which are close:

  1. assert_eq!. This needs another line and it panics so the caller cannot handle the error.
  2. Using traits like these:

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

I could write fork().to_option().expect("could not fork"). This is now only one line, but it panics instead of returning an Err. I guess this could be solved using ok_or.

Some functions of libc have < 0 as sentinel (e.g. fork), while others use > 0 (e.g. pthread_attr_init), so this would need another argument.

Is there something out there which solves this?

like image 991
hansaplast Avatar asked Mar 13 '17 19:03

hansaplast


People also ask

Does rust have error handling?

Errors are a fact of life in software, so Rust has a number of features for handling situations in which something goes wrong. In many cases, Rust requires you to acknowledge the possibility of an error and take some action before your code will compile.


1 Answers

As indicated in the other answer, use pre-made wrappers whenever possible. Where such wrappers do not exist, the following guidelines might help.

Return Result to indicate errors

The idiomatic Rust return type that includes error information is Result (std::result::Result). For most functions from POSIX libc, the specialized type std::io::Result is a perfect fit because it uses std::io::Error to encode errors, and it includes all standard system errors represented by errno values. A good way to avoid repetition is using a utility function such as:

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

Wrapping fork() would look like this:

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

The use of Result allows idiomatic usage such as:

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

Restrict the return type

The function will be easier to use if the return type is modified so that only "possible" values are included. For example, if a function logically has no return value, but returns an int only to communicate the presence of error, the Rust wrapper should return nothing:

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

Another example are functions that logically return an unsigned integer, such as a PID or a file descriptor, but still declare their result as signed to include the -1 error return value. In that case, consider returning an unsigned value in Rust, as in the fork() example above. nix takes this one step further by having fork() return Result<ForkResult>, where ForkResult is a real enum with methods such as is_child(), and from which the PID is extracted using pattern matching.

Use options and other enums

Rust has a rich type system that allows expressing things that have to be encoded as magic values in C. To return to the fork() example, that function returns 0 to indicate the child return. This would be naturally expressed with an Option and can be combined with the Result shown above:

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

The user of this API would no longer need to compare with the magic value, but would use pattern matching, for example:

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

Return values instead of using output parameters

C often returns values using output parameters, pointer parameters into which the results are stored. This is either because the actual return value is reserved for the error indicator, or because more than one value needs to be returned, and returning structs was badly supported by historical C compilers.

In contrast, Rust's Result supports return value independent of error information, and has no problem whatsoever with returning multiple values. Multiple values returned as a tuple are much more ergonomic than output parameters because they can be used in expressions or captured using pattern matching.

Wrap system resources in owned objects

When returning handles to system resources, such as file descriptors or Windows handles, it good practice to return them wrapped in an object that implements Drop to release them. This will make it less likely that a user of the wrapper will make a mistake, and it makes the use of return values more idiomatic, removing the need for awkward invocations of close() and resource leaks coming from failing to do so.

Taking pipe() as an example:

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

This pipe() wrapper returns multiple values and uses a wrapper object to refer to a system resource. Also, it returns the File objects defined in the Rust standard library and accepted by Rust's IO layer.

like image 152
user4815162342 Avatar answered Sep 28 '22 07:09

user4815162342