portable-atomic 0.1.4

Portable atomic types.
Documentation
// The rustc-cfg emitted by the build script are *not* public API.

#![warn(rust_2018_idioms, single_use_lifetimes)]

use std::{
    env,
    io::Write,
    process::{Command, Stdio},
    str,
    sync::atomic::{AtomicUsize, Ordering},
};

include!("no_atomic.rs");

// rustc +stable -Vv | grep -E '^(commit-date|release)'
const LATEST_STABLE: Version =
    Version { minor: 59, nightly: false, commit_date: Date { year: 2022, month: 2, day: 23 } };

const PROBE_ATOMIC_128: &str = r#"
#![no_std]
#![allow(stable_features)]
#![feature(integer_atomics)]
fn _probe() {
    let v = core::sync::atomic::AtomicU128::new(0);
    let _ = v.swap(1, core::sync::atomic::Ordering::Relaxed);
}
"#;
const PROBE_CMPXCHG16B: &str = r#"
#![no_std]
#![allow(stable_features)]
#![feature(stdsimd, cmpxchg16b_target_feature)]
#[allow(unused_unsafe)]
#[target_feature(enable = "cmpxchg16b")]
unsafe fn _probe(dst: *mut u128) -> u128 {
    unsafe {
        core::arch::x86_64::cmpxchg16b(
            dst,
            0_u128,
            0_u128,
            core::sync::atomic::Ordering::Relaxed,
            core::sync::atomic::Ordering::Relaxed,
        )
    }
}
"#;

fn main() {
    println!("cargo:rerun-if-changed=no_atomic.rs");

    let target = match env::var("TARGET") {
        Ok(target) => target,
        Err(e) => {
            println!(
                "cargo:warning={}: unable to get TARGET environment variable: {}",
                env!("CARGO_PKG_NAME"),
                e
            );
            return;
        }
    };
    // HACK: If --target is specified, rustflags is not applied to the build
    // script itself, so the build script will not be rerun when these are changed.
    println!("cargo:rerun-if-env-changed=RUSTFLAGS");
    println!("cargo:rerun-if-env-changed=CARGO_BUILD_RUSTFLAGS");
    println!(
        "cargo:rerun-if-env-changed=CARGO_TARGET_{}_RUSTFLAGS",
        target.to_uppercase().replace('-', "_").replace('.', "_")
    );

    let version = match rustc_version() {
        Some(version) => version,
        None => LATEST_STABLE,
    };

    // Note that this is `no_`*, not `has_*`. This allows treating as the latest
    // stable rustc is used when the build script doesn't run. This is useful
    // for non-cargo build systems that don't run the build script.
    if version.minor < 37 {
        println!("cargo:rustc-cfg=portable_atomic_no_underscore_consts");
    }
    if version.minor < 45 {
        println!("cargo:rustc-cfg=portable_atomic_no_atomic_min_max");
    }
    if version.minor < 52 {
        println!("cargo:rustc-cfg=portable_atomic_no_unsafe_op_in_unsafe_fn");
    }
    if version.minor < 56 {
        println!("cargo:rustc-cfg=portable_atomic_no_core_unwind_safe");
    }
    if version.minor < 59 {
        println!("cargo:rustc-cfg=portable_atomic_no_asm");
    }

    if version.minor >= 60 || version.nightly {
        println!("cargo:rustc-cfg=portable_atomic_cfg_target_has_atomic");
        // feature(cfg_target_has_atomic) stabilized in 1.60 (commit-date: 2022-02-10) https://github.com/rust-lang/rust/pull/93824
        if version.nightly
            && version.minor <= 60
            && (version.commit_date < Date { year: 2022, month: 2, day: 10 })
        {
            println!("cargo:rustc-cfg=portable_atomic_unstable_cfg_target_has_atomic");
        }
    } else {
        // TODO: invert portable_atomic_cfg_target_has_atomic once Rust 1.60 became stable.
        println!("cargo:rustc-cfg=portable_atomic_no_cfg_target_has_atomic");
        if NO_ATOMIC_CAS.contains(&&*target) {
            println!("cargo:rustc-cfg=portable_atomic_no_atomic_cas");
        }
        if NO_ATOMIC_64.contains(&&*target) {
            println!("cargo:rustc-cfg=portable_atomic_no_atomic_64");
        } else {
            // Otherwise, assuming `"max-atomic-width" == 64` or `"max-atomic-width" == 128`.
        }
    }
    if NO_ATOMIC.contains(&&*target) {
        println!("cargo:rustc-cfg=portable_atomic_no_atomic_load_store");
    }

    if target.starts_with("thumbv6m-") && target.contains("-none") {
        println!("cargo:rustc-cfg=thumbv6m");
    }
    // #[cfg(not(target_feature = "v6"))] doesn't work on stable.
    if target.starts_with("armv5te-") {
        println!("cargo:rustc-cfg=armv5te");
    }

    if target.starts_with("aarch64")
        && (version.minor >= 59 || version.nightly)
        && has_unstable_target_feature("lse", false, &version)
    {
        println!("cargo:rustc-cfg=portable_atomic_target_feature_lse");
    }

    // cmpxchg16b is available via asm (1.59+) or stdsimd (nightly).
    let may_use_cmpxchg16b =
        target.starts_with("x86_64") && (version.minor >= 59 || version.nightly);
    let mut has_cmpxchg16b = false;
    if may_use_cmpxchg16b {
        // x86_64 macos always support cmpxchg16b: https://github.com/rust-lang/rust/blob/1.59.0/compiler/rustc_target/src/spec/x86_64_apple_darwin.rs#L7
        has_cmpxchg16b =
            has_unstable_target_feature("cmpxchg16b", target == "x86_64-apple-darwin", &version);
    }
    if has_cmpxchg16b {
        println!("cargo:rustc-cfg=portable_atomic_target_feature_cmpxchg16b");
    }

    // `cfg(sanitize = "..")` is not stabilized.
    let tsan = std::env::var("CARGO_CFG_SANITIZE").unwrap_or_default().contains("thread");
    if tsan {
        println!("cargo:rustc-cfg=sanitize_thread");
    }

    if version.nightly {
        println!("cargo:rustc-cfg=portable_atomic_nightly");
    }
    if cfg!(feature = "i128") {
        if version.nightly
            && HAS_ATOMIC_128.contains(&&*target)
            && probe(PROBE_ATOMIC_128, Some(&target)).unwrap_or(false)
        {
            println!("cargo:rustc-cfg=portable_atomic_core_atomic_128");
        } else if may_use_cmpxchg16b
            && (version.nightly && cfg!(feature = "i128-dynamic") && !tsan || has_cmpxchg16b)
        {
            // On x86_64, if cmpxchg16b is available, we can use it to provide Atomic*128.
            println!("cargo:rustc-cfg=portable_atomic_cmpxchg16b");
            if version.nightly && probe(PROBE_CMPXCHG16B, Some(&target)).unwrap_or(false) {
                println!("cargo:rustc-cfg=portable_atomic_cmpxchg16b_stdsimd");
                if cfg!(feature = "i128-dynamic") {
                    println!("cargo:rustc-cfg=portable_atomic_cmpxchg16b_dynamic");
                }
            }
        }
    }
}

struct Version {
    minor: u32,
    nightly: bool,
    commit_date: Date,
}

#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct Date {
    year: u16,
    month: u8,
    day: u8,
}

fn rustc_version() -> Option<Version> {
    let rustc = env::var_os("RUSTC")?;
    // Use verbose version output because the packagers add extra strings to the normal version output.
    let output = Command::new(rustc).args(&["--version", "--verbose"]).output().ok()?;
    let output = str::from_utf8(&output.stdout).ok()?;

    let mut release = output
        .lines()
        .find(|line| line.starts_with("release: "))
        .map(|line| &line["release: ".len()..])?
        .splitn(2, '-');
    let version = release.next().unwrap();
    let channel = release.next().unwrap_or_default();
    let mut digits = version.splitn(3, '.');
    let major = digits.next()?.parse::<u32>().ok()?;
    if major != 1 {
        return None;
    }
    let minor = digits.next()?.parse::<u32>().ok()?;
    let _patch = digits.next().unwrap_or("0").parse::<u32>().ok()?;
    let nightly = channel == "nightly" || channel == "dev";

    let mut commit_date = output
        .lines()
        .find(|line| line.starts_with("commit-date: "))
        .map(|line| &line["commit-date: ".len()..])?
        .splitn(3, '-');
    let year = commit_date.next()?.parse::<u16>().ok()?;
    let month = commit_date.next()?.parse::<u8>().ok()?;
    let day = commit_date.next()?.parse::<u8>().ok()?;
    if month > 12 || day > 31 {
        return None;
    }

    Some(Version { minor, nightly, commit_date: Date { year, month, day } })
}

// Adapted from https://github.com/cuviper/autocfg/blob/1.1.0/src/lib.rs#L205-L237 and https://github.com/dtolnay/anyhow/blob/1.0.53/build.rs#L67-L102.
fn probe(code: &str, target: Option<&str>) -> Option<bool> {
    static ID: AtomicUsize = AtomicUsize::new(0);

    let rustc = env::var_os("RUSTC")?;
    let out_dir = env::var_os("OUT_DIR")?;

    let id = ID.fetch_add(1, Ordering::Relaxed);

    // Make sure to pick up Cargo rustc configuration.
    let mut cmd = if let Some(wrapper) = env::var_os("CARGO_RUSTC_WRAPPER") {
        let mut cmd = Command::new(wrapper);
        // The wrapper's first argument is supposed to be the path to rustc.
        cmd.arg(rustc);
        cmd
    } else {
        Command::new(rustc)
    };
    cmd.stderr(Stdio::null())
        .arg("--edition=2018")
        .arg("--crate-name")
        .arg(format!("portable_atomic_build{}", id))
        .arg("--crate-type=lib")
        .arg("--out-dir")
        .arg(out_dir)
        .arg("--emit=llvm-ir");

    if let Some(target) = target {
        cmd.arg("--target").arg(target);
    }

    if let Some(rustflags) = env::var_os("CARGO_ENCODED_RUSTFLAGS") {
        if !rustflags.is_empty() {
            for flag in rustflags.to_string_lossy().split('\x1f') {
                cmd.arg(flag);
            }
        }
    }

    cmd.arg("-").stdin(Stdio::piped());
    let mut child = cmd.spawn().ok()?;
    let mut stdin = child.stdin.take().expect("rustc stdin");

    stdin.write_all(code.as_bytes()).ok()?;
    drop(stdin);

    let status = child.wait().ok()?;
    Some(status.success())
}

fn has_stable_target_feature(name: &str) -> bool {
    env::var("CARGO_CFG_TARGET_FEATURE").ok().map_or(false, |s| s.split(',').any(|s| s == name))
}

fn has_unstable_target_feature(
    name: &str,
    mut has_target_feature: bool,
    version: &Version,
) -> bool {
    // Currently, it seems that the only way that works on the stable is
    // to parse the `-C target-feature` in RUSTFLAGS.
    //
    // - #[cfg(target_feature = "cmpxchg16b")] doesn't work on stable.
    // - CARGO_CFG_TARGET_FEATURE excludes unstable features on stable.
    //
    // https://rust-lang.github.io/rfcs/2045-target-feature.html#backend-compilation-options
    if version.nightly {
        has_target_feature = has_stable_target_feature(name);
    } else if let Some(rustflags) = env::var_os("CARGO_ENCODED_RUSTFLAGS") {
        for mut flag in rustflags.to_string_lossy().split('\x1f') {
            if flag.starts_with("-C") {
                flag = &flag["-C".len()..];
            }
            if flag.starts_with("target-feature=") {
                flag = &flag["target-feature=".len()..];
                for s in flag.split(',') {
                    match (s.as_bytes()[0] as char, &s[1..]) {
                        ('+', f) if f == name => has_target_feature = true,
                        ('-', f) if f == name => has_target_feature = false,
                        _ => {}
                    }
                }
            }
        }
    }
    has_target_feature
}