Error type for generic operating system originating errors

I found myself needing an error type for a generic operating system error from the three common desktop operating systems: Windows, macOS, and Linux. In particular, GetLastError or HRESULT codes from Windows, errno from Linux, and OSStatus from macOS CoreFoundation family of frameworks (Though there are other error code types).

This is for functions that are cfg implemented per operating system but need to have an otherwise uniform interface between them, so for all other errors that aren't specifically handled, I'd like to return a type with helpful information for the caller so the error is useful to be able to figure out whatever unexpected error happened, rather then just punting with a Box<dyn Error> with a string, or with some anyhow::Error which is more for application as I can tell.

But I can't figure out what's the best way to cobble such type together... Especially considering it needs some variation depending on the OS.

I wonder if there is a good quality crate for this. Handling system errors in a language such as Rust feeels like it would be a common endeavor.

The basic structure should be something like:

use thiserror::Error;

#[derive(Clone, Debug, Error)]
#[error("OS error {code}: {message}")]
struct OsError {
    code: i64,
    message: String,
}

assuming i64 can represent error codes of all three platforms. Then, you'd use conditional compilation (#[cfg(unix)], #[cfg(windows)], etc.) to implement e.g. From<i64>, From<i32>, From<OSStatus> for OsError.

Hmm. But I would want to only calculate a message when it is actually requested (It's an additional API call on most operating systems). I'm not sure if thiserror supports manually defining Display/#[error] for only some variants, if I include this in a bigger enum, so I guess it might need to be it's own struct? And technically, since certain platforms can have different types of error codes, e.g. GetLastError and HRESULT, a more complete type might even be an enum by itself. Which is why I kinda wondered if there is anything ready made or is the custom to roll your own time you need it for only the stuff you really currently need (The Ye Olde C/C++ way of doing things IMHO)?

Then drop the message field and put the error mesaage logic directly in the Display impl.

Why would you need that? #[error("...")] doesn't care about the implementation of the field(s) being formatted. It only needs that they implement Display somehow. You can just wrap your OS error type in a newtype variant and add #[error("{0}")].

So I'm trying to start with something like this:

#[derive(Debug, Clone)]
pub struct OsError(i32);

impl Error for OsError {}

impl Display for OsError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let msg = unsafe { CFString::wrap_under_create_rule(SecCopyErrorMessageString(self.0, ptr::null_mut()) as _) };
        write!(f, "{} ({})", msg, self.0)
    }
}

Now I'm wondering what's the best way to handle the different operating systems. If each one has it's own OsError type under the it's own cfg-ed module. I wonder what I should expose from the crate as a public API. Or if I should instead try to create an OsError type that is uniform across the platforms.

You could either use or take inspiration from the functionality that's in std:

Apparently i32 is enough for the code itself on all currently supported platforms.

I would certainly prefer to use a uniform API. Platform-specific (non-additive) APIs are generally painful to work with.