Skip to content
This repository was archived by the owner on Apr 28, 2025. It is now read-only.

Commit 637b7b3

Browse files
committed
Introduce musl-math-sys for bindings to musl math symbols
This crate builds math symbols from a musl checkout and provides a Rust interface. The intent is that we will be able to compare our implementations against musl on more than just linux (which are the only currently the only targets we run `*-musl` targets against for comparison). Musl libc can't compile on anything other than Linux; however, the routines in `src/math` are cross platform enough to build on MacOS and windows-gnu with only minor adjustments. We take advantage of this and build only needed files using `cc`. The build script also performs remapping (via defines) so that e.g. `cos` gets defined as `musl_cos`. This gives us more certainty that we are actually testing against the intended symbol; without it, it is easy to unknowingly link to system libraries or even Rust's `libm` itself and wind up with an ineffective test. There is also a small procedure to verify remapping worked correctly by checking symbols in object files.
1 parent 5b79b6d commit 637b7b3

File tree

7 files changed

+703
-2
lines changed

7 files changed

+703
-2
lines changed

.gitignore

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
**/*.rs.bk
1+
**.bk
22
.#*
33
/bin
44
/math/src
55
/math/target
66
/target
7-
/tests
87
Cargo.lock
8+
musl/
9+
**.tar.gz

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ unstable = []
2424
force-soft-floats = []
2525

2626
[workspace]
27+
resolver = "2"
2728
members = [
2829
"crates/compiler-builtins-smoke-test",
2930
"crates/libm-bench",
3031
"crates/libm-test",
32+
"crates/musl-math-sys",
3133
]
3234
default-members = [
3335
".",

crates/musl-math-sys/Cargo.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[package]
2+
name = "musl-math-sys"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
8+
[dev-dependencies]
9+
libm = { path = "../../" }
10+
11+
[build-dependencies]
12+
cc = "1.1.24"

crates/musl-math-sys/build.rs

Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
use std::collections::BTreeMap;
2+
use std::path::{Path, PathBuf};
3+
use std::process::{Command, Stdio};
4+
use std::{env, fs, str};
5+
6+
/// Static library that will be built
7+
const LIB_NAME: &str = "musl_math_prefixed";
8+
9+
/// Files that have more than one symbol. Map of file names to the symbols defined in that file.
10+
const MULTIPLE_SYMBOLS: &[(&str, &[&str])] = &[
11+
("__invtrigl", &["__invtrigl", "__invtrigl_R", "__pio2_hi", "__pio2_lo"]),
12+
("__polevll", &["__polevll", "__p1evll"]),
13+
("erf", &["erf", "erfc"]),
14+
("erff", &["erff", "erfcf"]),
15+
("erfl", &["erfl", "erfcl"]),
16+
("exp10", &["exp10", "pow10"]),
17+
("exp10f", &["exp10f", "pow10f"]),
18+
("exp10l", &["exp10l", "pow10l"]),
19+
("exp2f_data", &["exp2f_data", "__exp2f_data"]),
20+
("exp_data", &["exp_data", "__exp_data"]),
21+
("j0", &["j0", "y0"]),
22+
("j0f", &["j0f", "y0f"]),
23+
("j1", &["j1", "y1"]),
24+
("j1f", &["j1f", "y1f"]),
25+
("jn", &["jn", "yn"]),
26+
("jnf", &["jnf", "ynf"]),
27+
("lgamma", &["lgamma", "__lgamma_r"]),
28+
("remainder", &["remainder", "drem"]),
29+
("remainderf", &["remainderf", "dremf"]),
30+
("lgammaf", &["lgammaf", "lgammaf_r", "__lgammaf_r"]),
31+
("lgammal", &["lgammal", "lgammal_r", "__lgammal_r"]),
32+
("log2_data", &["log2_data", "__log2_data"]),
33+
("log2f_data", &["log2f_data", "__log2f_data"]),
34+
("log_data", &["log_data", "__log_data"]),
35+
("logf_data", &["logf_data", "__logf_data"]),
36+
("pow_data", &["pow_data", "__pow_log_data"]),
37+
("powf_data", &["powf_data", "__powf_log2_data"]),
38+
("signgam", &["signgam", "__signgam"]),
39+
("sqrt_data", &["sqrt_data", "__rsqrt_tab"]),
40+
];
41+
42+
fn main() {
43+
let cfg = Config::from_env();
44+
45+
if cfg.target_env == "msvc"
46+
|| cfg.target_family == "wasm"
47+
|| cfg.target_features.iter().any(|f| f == "thumb-mode")
48+
{
49+
println!(
50+
"cargo::warning=Musl doesn't compile with the current \
51+
target {}; skipping build",
52+
&cfg.target_string
53+
);
54+
return;
55+
}
56+
57+
build_musl_math(&cfg);
58+
}
59+
60+
#[allow(dead_code)]
61+
#[derive(Debug)]
62+
struct Config {
63+
manifest_dir: PathBuf,
64+
out_dir: PathBuf,
65+
musl_dir: PathBuf,
66+
musl_arch: String,
67+
target_arch: String,
68+
target_env: String,
69+
target_family: String,
70+
target_os: String,
71+
target_string: String,
72+
target_vendor: String,
73+
target_features: Vec<String>,
74+
}
75+
76+
impl Config {
77+
fn from_env() -> Self {
78+
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
79+
let target_features = env::var("CARGO_CFG_TARGET_FEATURE")
80+
.map(|feats| feats.split(',').map(ToOwned::to_owned).collect())
81+
.unwrap_or_default();
82+
83+
// Default to the `{workspace_root}/musl` if not specified
84+
let musl_dir = env::var("MUSL_SOURCE_DIR")
85+
.map(PathBuf::from)
86+
.unwrap_or_else(|_| manifest_dir.parent().unwrap().parent().unwrap().join("musl"));
87+
88+
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap();
89+
let musl_arch = if target_arch == "x86" { "i386".to_owned() } else { target_arch.clone() };
90+
91+
println!("cargo::rerun-if-changed={}/c_patches", manifest_dir.display());
92+
println!("cargo::rerun-if-env-changed=MUSL_SOURCE_DIR");
93+
println!("cargo::rerun-if-changed={}", musl_dir.display());
94+
95+
Self {
96+
manifest_dir,
97+
out_dir: PathBuf::from(env::var("OUT_DIR").unwrap()),
98+
musl_dir,
99+
musl_arch,
100+
target_arch,
101+
target_env: env::var("CARGO_CFG_TARGET_ENV").unwrap(),
102+
target_family: env::var("CARGO_CFG_TARGET_FAMILY").unwrap(),
103+
target_os: env::var("CARGO_CFG_TARGET_OS").unwrap(),
104+
target_string: env::var("TARGET").unwrap(),
105+
target_vendor: env::var("CARGO_CFG_TARGET_VENDOR").unwrap(),
106+
target_features,
107+
}
108+
}
109+
}
110+
111+
/// Build musl math symbols to a static library
112+
fn build_musl_math(cfg: &Config) {
113+
let musl_dir = &cfg.musl_dir;
114+
assert!(
115+
musl_dir.exists(),
116+
"musl source is missing. it can be downloaded with ./ci/download-musl.sh"
117+
);
118+
119+
let math = musl_dir.join("src/math");
120+
let arch_dir = musl_dir.join("arch").join(&cfg.musl_arch);
121+
let source_map = find_math_source(&math, cfg);
122+
let out_path = cfg.out_dir.join(format!("lib{LIB_NAME}.a"));
123+
124+
// Run configuration steps. Usually done as part of the musl `Makefile`.
125+
let obj_include = cfg.out_dir.join("musl_obj/include");
126+
fs::create_dir_all(&obj_include).unwrap();
127+
fs::create_dir_all(&obj_include.join("bits")).unwrap();
128+
let sed_stat = Command::new("sed")
129+
.arg("-f")
130+
.arg(musl_dir.join("tools/mkalltypes.sed"))
131+
.arg(arch_dir.join("bits/alltypes.h.in"))
132+
.arg(musl_dir.join("include/alltypes.h.in"))
133+
.stderr(Stdio::inherit())
134+
.output()
135+
.unwrap();
136+
assert!(sed_stat.status.success(), "sed command failed: {:?}", sed_stat.status);
137+
138+
fs::write(obj_include.join("bits/alltypes.h"), sed_stat.stdout).unwrap();
139+
140+
let mut cbuild = cc::Build::new();
141+
cbuild
142+
.extra_warnings(false)
143+
.warnings(false)
144+
.flag_if_supported("-Wno-bitwise-op-parentheses")
145+
.flag_if_supported("-Wno-literal-range")
146+
.flag_if_supported("-Wno-parentheses")
147+
.flag_if_supported("-Wno-shift-count-overflow")
148+
.flag_if_supported("-Wno-shift-op-parentheses")
149+
.flag_if_supported("-Wno-unused-but-set-variable")
150+
.flag_if_supported("-std=c99")
151+
.flag_if_supported("-ffreestanding")
152+
.flag_if_supported("-nostdinc")
153+
.define("_ALL_SOURCE", "1")
154+
.opt_level(3)
155+
.define(
156+
"ROOT_INCLUDE_FEATURES",
157+
Some(musl_dir.join("include/features.h").to_str().unwrap()),
158+
)
159+
// Our overrides are in this directory
160+
.include(cfg.manifest_dir.join("c_patches"))
161+
.include(musl_dir.join("arch").join(&cfg.musl_arch))
162+
.include(musl_dir.join("arch/generic"))
163+
.include(musl_dir.join("src/include"))
164+
.include(musl_dir.join("src/internal"))
165+
.include(obj_include)
166+
.include(musl_dir.join("include"))
167+
.file(cfg.manifest_dir.join("c_patches/alias.c"));
168+
169+
for (sym_name, src_file) in source_map {
170+
// Build the source file
171+
cbuild.file(src_file);
172+
173+
// Trickery! Redefine the symbol names to have the prefix `musl_`, which allows us to
174+
// differentiate these symbols from whatever we provide.
175+
if let Some((_names, syms)) =
176+
MULTIPLE_SYMBOLS.iter().find(|(name, _syms)| *name == sym_name)
177+
{
178+
// Handle the occasional file that defines multiple symbols
179+
for sym in *syms {
180+
cbuild.define(sym, Some(format!("musl_{sym}").as_str()));
181+
}
182+
} else {
183+
// If the file doesn't define multiple symbols, the file name will be the symbol
184+
cbuild.define(&sym_name, Some(format!("musl_{sym_name}").as_str()));
185+
}
186+
}
187+
188+
if cfg!(windows) {
189+
// On Windows we don't have a good way to check symbols, so skip that step.
190+
cbuild.compile(LIB_NAME);
191+
return;
192+
}
193+
194+
let objfiles = cbuild.compile_intermediates();
195+
196+
// We create the archive ourselves with relocations rather than letting `cc` do it so we can
197+
// encourage it to resolve symbols now. This should help avoid accidentally linking the wrong
198+
// thing.
199+
let stat = cbuild
200+
.get_compiler()
201+
.to_command()
202+
.arg("-r")
203+
.arg("-o")
204+
.arg(&out_path)
205+
.args(objfiles)
206+
.status()
207+
.unwrap();
208+
assert!(stat.success());
209+
210+
println!("cargo::rustc-link-lib={LIB_NAME}");
211+
println!("cargo::rustc-link-search=native={}", cfg.out_dir.display());
212+
213+
validate_archive_symbols(&out_path);
214+
}
215+
216+
/// Build a map of `name -> path`. `name` is typically the symbol name, but this doesn't account
217+
/// for files that provide multiple symbols.
218+
fn find_math_source(math_root: &Path, cfg: &Config) -> BTreeMap<String, PathBuf> {
219+
let mut map = BTreeMap::new();
220+
let mut arch_dir = None;
221+
222+
// Locate all files and directories
223+
for item in fs::read_dir(math_root).unwrap() {
224+
let path = item.unwrap().path();
225+
let meta = fs::metadata(&path).unwrap();
226+
227+
if meta.is_dir() {
228+
// Make note of the arch-specific directory if it exists
229+
if path.file_name().unwrap() == cfg.target_arch.as_str() {
230+
arch_dir = Some(path);
231+
}
232+
continue;
233+
}
234+
235+
// Skip non-source files
236+
if path.extension().is_some_and(|ext| ext == "h") {
237+
continue;
238+
}
239+
240+
let sym_name = path.file_stem().unwrap();
241+
map.insert(sym_name.to_str().unwrap().to_owned(), path.to_owned());
242+
}
243+
244+
// If arch-specific versions are available, build those instead.
245+
if let Some(arch_dir) = arch_dir {
246+
for item in fs::read_dir(arch_dir).unwrap() {
247+
let path = item.unwrap().path();
248+
let sym_name = path.file_stem().unwrap();
249+
250+
if path.extension().unwrap() == "s" {
251+
// FIXME: we never build assembly versions since we have no good way to
252+
// rename the symbol (our options are probably preprocessor or objcopy).
253+
continue;
254+
}
255+
map.insert(sym_name.to_str().unwrap().to_owned(), path);
256+
}
257+
}
258+
259+
map
260+
}
261+
262+
/// Make sure we don't have something like a loose unprefixed `_cos` called somewhere, which could
263+
/// wind up linking to system libraries rather than the built musl library.
264+
fn validate_archive_symbols(out_path: &Path) {
265+
const ALLOWED_UNDEF_PFX: &[&str] = &[
266+
// PIC and arch-specific
267+
".TOC",
268+
"_GLOBAL_OFFSET_TABLE_",
269+
"__x86.get_pc_thunk",
270+
// gcc/compiler-rt/compiler-builtins symbols
271+
"__add",
272+
"__aeabi_",
273+
"__div",
274+
"__eq",
275+
"__extend",
276+
"__fix",
277+
"__float",
278+
"__gcc_",
279+
"__ge",
280+
"__gt",
281+
"__le",
282+
"__lshr",
283+
"__lt",
284+
"__mul",
285+
"__ne",
286+
"__stack_chk_fail",
287+
"__stack_chk_guard",
288+
"__sub",
289+
"__trunc",
290+
"__undef",
291+
// string routines
292+
"__bzero",
293+
"bzero",
294+
// FPENV interfaces
295+
"feclearexcept",
296+
"fegetround",
297+
"feraiseexcept",
298+
"fesetround",
299+
"fetestexcept",
300+
];
301+
302+
// List global undefined symbols
303+
let out =
304+
Command::new("nm").arg("-guj").arg(out_path).stderr(Stdio::inherit()).output().unwrap();
305+
306+
let undef = str::from_utf8(&out.stdout).unwrap();
307+
let mut undef = undef.lines().collect::<Vec<_>>();
308+
undef.retain(|sym| {
309+
// Account for file formats that add a leading `_`
310+
!ALLOWED_UNDEF_PFX.iter().any(|pfx| sym.starts_with(pfx) || sym[1..].starts_with(pfx))
311+
});
312+
313+
assert!(undef.is_empty(), "found disallowed undefined symbols: {undef:#?}");
314+
315+
// Find any symbols that are missing the `_musl_` prefix`
316+
let out =
317+
Command::new("nm").arg("-gUj").arg(out_path).stderr(Stdio::inherit()).output().unwrap();
318+
319+
let defined = str::from_utf8(&out.stdout).unwrap();
320+
let mut defined = defined.lines().collect::<Vec<_>>();
321+
defined.retain(|sym| {
322+
!(sym.starts_with("_musl_")
323+
|| sym.starts_with("musl_")
324+
|| sym.starts_with("__x86.get_pc_thunk"))
325+
});
326+
327+
assert!(defined.is_empty(), "found unprefixed symbols: {defined:#?}");
328+
}

0 commit comments

Comments
 (0)