Skip to main content

rusteron_code_gen/
c_build.rs

1//! Release compiler flags for the vendored Aeron C build.
2//!
3//! The choice lives here rather than in `build_common.rs`, which each crate's
4//! build script `include!`s and so cannot be unit-tested in place.
5
6/// Which CPUs a release build of Aeron C must run on.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum CpuTarget {
9    /// Built from source for this CPU (`-march=native`).
10    ThisCpu,
11    /// A prebuilt library published for `precompile` users, which must run on any CPU of
12    /// the architecture.
13    Baseline,
14}
15
16/// `CMAKE_C_FLAGS_RELEASE` for the Aeron C build of `arch` (`CARGO_CFG_TARGET_ARCH`)
17/// and `vendor` (`CARGO_CFG_TARGET_VENDOR`) for `target`. `march` (`RUSTERON_C_MARCH`)
18/// overrides the target's `-march`.
19pub fn release_c_flags(arch: &str, vendor: &str, target: CpuTarget, march: Option<&str>) -> String {
20    let mut flags = String::from("-O3 -DNDEBUG -funroll-loops");
21    match (march.filter(|m| !m.is_empty()), target) {
22        (Some(march), _) => flags.push_str(&format!(" -march={march}")),
23        (None, CpuTarget::ThisCpu) => flags.push_str(" -march=native"),
24        (None, CpuTarget::Baseline) => match arch {
25            "x86_64" => flags.push_str(" -march=x86-64 -mtune=generic"),
26            "aarch64" if vendor != "apple" => flags.push_str(" -march=armv8-a"),
27            // Apple's default CPU is already the platform baseline.
28            _ => {}
29        },
30    }
31    flags
32}
33
34/// Whether the final binary links with `-Clinker-plugin-lto`, from `CARGO_ENCODED_RUSTFLAGS`
35/// (flags separated by `0x1f`). That flag is the switch for cross-language LTO: Aeron C is
36/// then compiled to LLVM bitcode so the linker can inline it into Rust callers.
37pub fn wants_linker_plugin_lto(encoded_rustflags: &str) -> bool {
38    codegen_options(encoded_rustflags)
39        .any(|o| o == "linker-plugin-lto" || o.starts_with("linker-plugin-lto=") && o != "linker-plugin-lto=no")
40}
41
42/// The `-C` options in `CARGO_ENCODED_RUSTFLAGS`, written `-Cname=value` or `-C name=value`.
43fn codegen_options(encoded_rustflags: &str) -> impl Iterator<Item = &str> {
44    let flags: Vec<&str> = encoded_rustflags.split('\x1f').collect();
45    (0..flags.len()).filter_map(move |i| match flags[i].strip_prefix("-C") {
46        Some("") => flags.get(i + 1).copied(),
47        Some(option) => Some(option),
48        None => None,
49    })
50}
51
52/// `CMAKE_C_FLAGS_RELEASE` under cross-language LTO. LLVM inlines a C function into a Rust
53/// caller only when the C target features are a subset of Rust's, so C targets exactly the
54/// CPU rustc does (`rustc_cpu`, from `-Ctarget-cpu`; `None` keeps both on the target default).
55pub fn lto_release_c_flags(arch: &str, rustc_cpu: Option<&str>) -> String {
56    let mut flags = String::from("-O3 -DNDEBUG -funroll-loops");
57    match rustc_cpu {
58        // clang's -march=native on aarch64 picks a different CPU from rustc's native
59        Some(cpu) if arch == "aarch64" => flags.push_str(&format!(" -mcpu={cpu}")),
60        Some(cpu) => flags.push_str(&format!(" -march={cpu}")),
61        None => {}
62    }
63    if arch == "x86_64" {
64        // clang gives Ice Lake server and later Intel CPUs SGX, which LLVM's own CPU models
65        // (and so rustc's) lack; Aeron uses no SGX
66        flags.push_str(" -mno-sgx");
67    }
68    flags
69}
70
71/// The last `-Ctarget-cpu` in `CARGO_ENCODED_RUSTFLAGS`.
72pub fn rustc_target_cpu(encoded_rustflags: &str) -> Option<&str> {
73    codegen_options(encoded_rustflags)
74        .filter_map(|o| o.strip_prefix("target-cpu="))
75        .last()
76}
77
78/// The LLVM major version in `rustc -vV` (`LLVM version: 22.1.2`) or `clang --version`
79/// (`clang version 22.1.8`) output.
80pub fn llvm_major(version_output: &str) -> Option<u32> {
81    version_output.lines().find_map(|line| {
82        let rest = line
83            .strip_prefix("LLVM version: ")
84            .or_else(|| line.split_once("clang version ").map(|(_, v)| v))?;
85        rest.split('.').next()?.trim().parse().ok()
86    })
87}
88
89/// The `CMAKE_C_FLAGS` for cross-language LTO: `-flto` for `RUSTERON_C_LTO` (`full`, the
90/// default, or `thin`), and `--target` set to rustc's `target` (cargo's `TARGET`). Without
91/// it clang tags the bitcode with its own default triple (`x86_64-pc-linux-gnu` on Debian
92/// and Ubuntu), and lld warns on every C module linked into Rust's `x86_64-unknown-linux-gnu`.
93/// The cmake crate would pass `--target` itself, but skips its compiler flags once
94/// `CMAKE_C_FLAGS` is defined.
95pub fn c_lto_flags(kind: Option<&str>, target: &str) -> Result<String, String> {
96    let lto = match kind.filter(|k| !k.is_empty()) {
97        None | Some("full") => "-flto",
98        Some("thin") => "-flto=thin",
99        Some(other) => return Err(format!("RUSTERON_C_LTO={other} is not one of `full` or `thin`")),
100    };
101    Ok(format!("{lto} --target={target}"))
102}
103
104#[cfg(test)]
105mod tests {
106    use super::{
107        CpuTarget, c_lto_flags, llvm_major, lto_release_c_flags, release_c_flags, rustc_target_cpu,
108        wants_linker_plugin_lto,
109    };
110
111    #[test]
112    fn lto_c_follows_rustc_cpu() {
113        assert_eq!(
114            lto_release_c_flags("aarch64", Some("native")),
115            "-O3 -DNDEBUG -funroll-loops -mcpu=native"
116        );
117        assert_eq!(
118            lto_release_c_flags("x86_64", Some("emeraldrapids")),
119            "-O3 -DNDEBUG -funroll-loops -march=emeraldrapids -mno-sgx"
120        );
121        assert_eq!(
122            lto_release_c_flags("x86_64", None),
123            "-O3 -DNDEBUG -funroll-loops -mno-sgx"
124        );
125        assert_eq!(
126            rustc_target_cpu("-Ctarget-cpu=native\x1f-Clinker-plugin-lto"),
127            Some("native")
128        );
129        assert_eq!(rustc_target_cpu("-C\x1ftarget-cpu=a\x1f-Ctarget-cpu=b"), Some("b"));
130        assert_eq!(rustc_target_cpu("-Clinker-plugin-lto"), None);
131    }
132
133    #[test]
134    fn detects_linker_plugin_lto() {
135        assert!(wants_linker_plugin_lto("-Ctarget-cpu=native\x1f-Clinker-plugin-lto"));
136        assert!(wants_linker_plugin_lto("-C\x1flinker-plugin-lto"));
137        assert!(wants_linker_plugin_lto("-Clinker-plugin-lto=yes"));
138        assert!(!wants_linker_plugin_lto("-Clinker-plugin-lto=no"));
139        assert!(!wants_linker_plugin_lto("-Ctarget-cpu=native"));
140        assert!(!wants_linker_plugin_lto(""));
141    }
142
143    #[test]
144    fn parses_llvm_major() {
145        assert_eq!(llvm_major("rustc 1.95.0\nhost: x\nLLVM version: 22.1.2\n"), Some(22));
146        assert_eq!(llvm_major("Homebrew clang version 22.1.8\nTarget: arm64"), Some(22));
147        assert_eq!(llvm_major("Ubuntu clang version 18.1.3 (1ubuntu1)\n"), Some(18));
148        assert_eq!(llvm_major("Apple clang version 21.0.0 (clang-2100.3.34.2)"), Some(21));
149        assert_eq!(llvm_major("gcc (GCC) 13.2.0"), None);
150    }
151
152    #[test]
153    fn lto_flags_carry_kind_and_rustc_target() {
154        let target = "x86_64-unknown-linux-gnu";
155        assert_eq!(
156            c_lto_flags(None, target),
157            Ok("-flto --target=x86_64-unknown-linux-gnu".into())
158        );
159        assert_eq!(
160            c_lto_flags(Some("thin"), target),
161            Ok("-flto=thin --target=x86_64-unknown-linux-gnu".into())
162        );
163        assert!(c_lto_flags(Some("fat"), target).is_err());
164    }
165
166    #[test]
167    fn from_source_targets_this_cpu() {
168        assert_eq!(
169            release_c_flags("x86_64", "unknown", CpuTarget::ThisCpu, None),
170            "-O3 -DNDEBUG -funroll-loops -march=native"
171        );
172    }
173
174    #[test]
175    fn published_libraries_target_the_architecture_baseline() {
176        assert_eq!(
177            release_c_flags("x86_64", "unknown", CpuTarget::Baseline, None),
178            "-O3 -DNDEBUG -funroll-loops -march=x86-64 -mtune=generic"
179        );
180        assert_eq!(
181            release_c_flags("aarch64", "unknown", CpuTarget::Baseline, None),
182            "-O3 -DNDEBUG -funroll-loops -march=armv8-a"
183        );
184        assert_eq!(
185            release_c_flags("aarch64", "apple", CpuTarget::Baseline, None),
186            "-O3 -DNDEBUG -funroll-loops"
187        );
188    }
189
190    #[test]
191    fn march_override_wins() {
192        assert_eq!(
193            release_c_flags("x86_64", "unknown", CpuTarget::ThisCpu, Some("x86-64-v3")),
194            "-O3 -DNDEBUG -funroll-loops -march=x86-64-v3"
195        );
196        assert_eq!(
197            release_c_flags("x86_64", "unknown", CpuTarget::Baseline, Some("x86-64-v2")),
198            "-O3 -DNDEBUG -funroll-loops -march=x86-64-v2"
199        );
200        assert_eq!(
201            release_c_flags("x86_64", "unknown", CpuTarget::ThisCpu, Some("")),
202            "-O3 -DNDEBUG -funroll-loops -march=native"
203        );
204    }
205}