Conditionally compile code based on presence or absence of a symbol in a dependency?

I'm looking for a way to conditionally compile some code based on whether or not each of two particular symbols are exported and not deprecated by a crate that I depend on.

Your reaction is probably "WTF why would you ever", so let me elaborate. The crate in question is libc and the symbols in question are ENOATTR and ENODATA, OS-level error codes which are only defined by some of the operating systems supported by Rust. libc's policy seems to be that it exports all of the E-codes defined by the target OS but not any of the others. (ENOATTR is exported as a deprecated alias on some of the OSes where it's not defined, but obviously I don't want to be using deprecated symbols.)

The code that uses these symbols looks like this:

/// Remove the xattr `name` from the file `f`; if it wasn't there in the first
/// place, that's fine.
fn discard_xattr<S: AsRef<OsStr>>(f: &File, name: S) -> io::Result<()> {
    f.remove_xattr(name).or_else(|e| {
        // Ignore failures due to the attribute not being present.
        // There is no ErrorKind for these errors and the low-level error code
        // is not consistent among supported operating systems.
        cfg_if! {
            if #[cfg(...)] {
                if e.raw_os_error() == Some(libc::ENOATTR) {
                    return Ok(());
                }
            }
            if #[cfg(...)] {
                if e.raw_os_error() == Some(libc::ENODATA) {
                    return Ok(());
                }
            }
        }
        Err(e)
    })
}

And the problem is: what do I write in those #[cfg(...)] expressions? The only option I seem to have is a any(target_os="a", target_os="b", ...) sequence that duplicates libc's logic for deciding whether to export these symbols in the first place, but that would be incredibly tedious to work out (since it's not written anything like that inside libc) and would also pretty much force me to have an == dependency on a specific patch rev of the libc crate.

Ideally I'd like something that makes this as easy as it is in C:

int discard_xattr(int fd, const char *name) {
    if (fremovexattr(fd, name) == 0)
        return 0;
#ifdef ENOATTR
    if (errno == ENOATTR)
        return 0;
#endif
#ifdef ENODATA
    if (errno == ENODATA)
        return 0;
#endif
    return -1;
}
2 Likes

Insofar as I'm aware, there's nothing in the language that can do that. Your best bet is probably to do what you said: switch based on a list of all the platforms that do/don't define that constant. cfg-if will probably help with that.

What you should absolutely not and under no circumstances whatsoever do is write a build script that generates and attempts to build a temporary crate that depends only on libc and tries to use the constants in question, then detects whether compilation succeeded or not, then exposes the result of this to your actual code via a cfg flag. That sort of abuse of the build system would be very not cash money, would be looked down upon, and will cause much furrowing of brows and intense disapproval.

So don't do it. Do the boring, tedious thing instead.

7 Likes

You can shadow your definition with libc's by using a glob import.

pub mod libc {
    pub const ENOATTR: u32 = 123;
}

fn main() {
    const ENOATTR: u32 = 999;
    {
        use libc::*;
        println!("defined? {}", 123 == ENOATTR);
    }
}
5 Likes

This needs both #[allow(dead_code)] and #[allow(deprecated)], but it works and it's ever so much more maintainable than a giant list of target_os values. Thanks.