diff --git a/Cargo.toml b/Cargo.toml index bd1bb80ce..042b6e65b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ checked = [] members = [ "crates/compiler-builtins-smoke-test", "crates/libm-bench", + "crates/libm-cdylib", ] [dev-dependencies] diff --git a/azure-pipelines.yml b/azure-pipelines.yml index c89346c73..4ccc59f2a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -37,8 +37,28 @@ jobs: TARGET: powerpc64-unknown-linux-gnu powerpc64le: TARGET: powerpc64le-unknown-linux-gnu - x86_64: + x86_64_stable: TARGET: x86_64-unknown-linux-gnu + x86_64_nightly: + TARGET: x86_64-unknown-linux-gnu + TOOLCHAIN: nightly + + - job: OSX + pool: + vmImage: macos-10.13 + steps: + - template: ci/azure-install-rust.yml + - bash: | + rustup target add $TARGET + sh ./ci/run.sh $TARGET + strategy: + matrix: + i686-apple-darwin: + TARGET: i686-apple-darwin + TOOLCHAIN: nightly + x86_64-apple-darwin: + TARGET: x86_64-apple-darwin + TOOLCHAIN: nightly - job: wasm pool: diff --git a/ci/run.sh b/ci/run.sh index 37ffb8793..f0016a793 100755 --- a/ci/run.sh +++ b/ci/run.sh @@ -1,15 +1,35 @@ #!/usr/bin/env sh set -ex + TARGET=$1 +export RUST_BACKTRACE=1 +export RUST_TEST_THREADS=1 + CMD="cargo test --all --no-default-features --target $TARGET" $CMD $CMD --release -$CMD --features 'stable' -$CMD --release --features 'stable' +$CMD --features "stable" +$CMD --release --features "stable" + +TEST_MUSL="musl-reference-tests" +if [ "$TARGET" = "x86_64-apple-darwin" ] || [ "$TARGET" = "i686-apple-darwin" ] ; then + # FIXME: disable musl-reference-tests on OSX + export TEST_MUSL="" +fi + +$CMD --features "stable checked" +$CMD --release --features "stable checked ${TEST_MUSL}" -$CMD --features 'stable checked musl-reference-tests' -$CMD --release --features 'stable checked musl-reference-tests' +if rustc --version | grep "nightly" ; then + if [ "$TARGET" = "x86_64-unknown-linux-gnu" ] || [ "${TARGET}" = "x86_64-apple-darwin" ]; then + ( + cd crates/libm-cdylib + cargo test + cargo test --release + ) + fi +fi diff --git a/crates/libm-cdylib/Cargo.toml b/crates/libm-cdylib/Cargo.toml new file mode 100644 index 000000000..43f427d4a --- /dev/null +++ b/crates/libm-cdylib/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "libm-cdylib" +version = "0.1.0" +authors = ["Gonzalo Brito Gadeschi "] +edition = "2018" + +[features] +default = ['stable'] +stable = [] + +# Used checked array indexing instead of unchecked array indexing in this +# library. +checked = [] + +[lib] +name = "libm" +crate-type = ["cdylib"] + +[dev-dependencies] +paste = "0.1.5" \ No newline at end of file diff --git a/crates/libm-cdylib/build.rs b/crates/libm-cdylib/build.rs new file mode 100644 index 000000000..0031e8a9f --- /dev/null +++ b/crates/libm-cdylib/build.rs @@ -0,0 +1,17 @@ +use std::env; +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + let profile = env::var("PROFILE").unwrap_or(String::new()); + if profile == "release" { + println!("cargo:rustc-cfg=release_profile"); + } + let nightly = { + let mut cmd = std::process::Command::new("rustc"); + cmd.arg("--version"); + let output = String::from_utf8(cmd.output().unwrap().stdout).unwrap(); + output.contains("nightly") + }; + if nightly { + println!("cargo:rustc-cfg=unstable_rust"); + } +} diff --git a/crates/libm-cdylib/src/lib.rs b/crates/libm-cdylib/src/lib.rs new file mode 100644 index 000000000..7223a770c --- /dev/null +++ b/crates/libm-cdylib/src/lib.rs @@ -0,0 +1,163 @@ +#![cfg( + // The tests are only enabled on x86 32/64-bit linux/macos: + all(unstable_rust, + any(target_os = "linux", target_os = "macos"), + any(target_arch = "x86", target_arch = "x86_64") + ) +)] +#![allow(dead_code)] +#![cfg_attr(not(test), feature(core_intrinsics, lang_items))] +#![cfg_attr(not(test), no_std)] + +#[path = "../../../src/math/mod.rs"] +mod libm; + +#[macro_use] +mod macros; + +#[cfg(test)] +mod test_utils; + +#[cfg(not(test))] +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + // TODO: just call libc::abort - fails to link + // unsafe { libc::abort() } + unsafe { core::intrinsics::abort() } +} + +// TODO: there has to be a way to avoid this +#[cfg(not(test))] +#[lang = "eh_personality"] +extern "C" fn eh_personality() {} + +// This macro exports the functions that are part of the C ABI. +// +// It generates tests that replace the implementation of the +// function with a specific value, that then is used to check +// that the library is properly linked. + +export! { + fn acos(x: f64) -> f64: (42.) -> 42.; + fn acosf(x: f32) -> f32: (42.) -> 42.; + fn acosh(x: f64) -> f64: (42.) -> 42.; + fn acoshf(x: f32) -> f32: (42.) -> 42.; + fn asin(x: f64) -> f64: (42.) -> 42.; + fn asinf(x: f32) -> f32: (42.) -> 42.; + fn asinh(x: f64) -> f64: (42.) -> 42.; + fn asinhf(x: f32) -> f32: (42.) -> 42.; + // FIXME: fails to link. Missing symbol: _memcpy + // fn atan(x: f64) -> f64: (42.) -> 42.; + fn atanf(x: f32) -> f32: (42.) -> 42.; + fn atanh(x: f64) -> f64: (42.) -> 42.; + fn atanhf(x: f32) -> f32: (42.) -> 42.; + fn cbrt(x: f64) -> f64: (42.) -> 42.; + fn cbrtf(x: f32) -> f32: (42.) -> 42.; + fn ceil(x: f64) -> f64: (42.) -> 42.; + fn ceilf(x: f32) -> f32: (42.) -> 42.; + fn copysign(x: f64, y: f64) -> f64: (42., 42.) -> 42.; + fn copysignf(x: f32, y: f32) -> f32: (42., 42.) -> 42.; + // FIXME: fails to link. Missing symbols + //fn cos(x: f64) -> f64: (42.) -> 42.; + //fn cosf(x: f32) -> f32: (42.) -> 42.; + fn cosh(x: f64) -> f64: (42.) -> 42.; + fn coshf(x: f32) -> f32: (42.) -> 42.; + fn erf(x: f64) -> f64: (42.) -> 42.; + fn erfc(x: f64) -> f64: (42.) -> 42.; + fn erff(x: f32) -> f32: (42.) -> 42.; + fn erfcf(x: f32) -> f32: (42.) -> 42.; + fn exp(x: f64) -> f64: (42.) -> 42.; + fn expf(x: f32) -> f32: (42.) -> 42.; + // FIXME: not in C: + // fn exp10(x: f64) -> f64: (42.) -> 42.; + // fn exp10f(x: f32) -> f32: (42.) -> 42.; + fn exp2(x: f64) -> f64: (42.) -> 42.; + fn exp2f(x: f32) -> f32: (42.) -> 42.; + fn expm1(x: f64) -> f64: (42.) -> 42.; + fn expm1f(x: f32) -> f32: (42.) -> 42.; + fn fabs(x: f64) -> f64: (42.) -> 42.; + fn fabsf(x: f32) -> f32: (42.) -> 42.; + fn fdim(x: f64, y: f64) -> f64: (42., 42.) -> 42.; + fn fdimf(x: f32, y: f32) -> f32: (42., 42.) -> 42.; + fn floor(x: f64) -> f64: (42.) -> 42.; + fn floorf(x: f32) -> f32: (42.) -> 42.; + fn fma(x: f64, y: f64, z: f64) -> f64: (42., 42., 42.) -> 42.; + fn fmaf(x: f32, y: f32, z: f32) -> f32: (42., 42., 42.) -> 42.; + fn fmax(x: f64, y: f64) -> f64: (42., 42.) -> 42.; + fn fmaxf(x: f32, y: f32) -> f32: (42., 42.) -> 42.; + fn fmin(x: f64, y: f64) -> f64: (42., 42.) -> 42.; + fn fminf(x: f32, y: f32) -> f32: (42., 42.) -> 42.; + fn fmod(x: f64, y: f64) -> f64: (42., 42.) -> 42.; + fn fmodf(x: f32, y: f32) -> f32: (42., 42.) -> 42.; + + // TODO: different ABI than in C - need a more elaborate wrapper + // fn frexp(x: f64) -> (f64, i32): (42.) -> (42., 42); + // fn frexpf(x: f32) -> (f32, i32): (42.) -> (42., 42); + + fn hypot(x: f64, y: f64) -> f64: (42., 42.) -> 42.; + fn hypotf(x: f32, y: f32) -> f32: (42., 42.) -> 42.; + fn ilogb(x: f64) -> i32: (42.) -> 42; + fn ilogbf(x: f32) -> i32: (42.) -> 42; + + // FIXME: fails to link. Missing symbols + // fn j0(x: f64) -> f64: (42.) -> 42.; + // fn j0f(x: f32) -> f32: (42.) -> 42.; + // fn j1(x: f64) -> f64: (42.) -> 42.; + // fn j1f(x: f32) -> f32: (42.) -> 42.; + // fn jn(n: i32, x: f64) -> f64: (42, 42.) -> 42.; + // fn jnf(n: i32, x: f32) -> f32: (42, 42.) -> 42.; + + fn ldexp(x: f64, n: i32) -> f64: (42, 42.) -> 42.; + fn ldexpf(x: f32, n: i32) -> f32: (42, 42.) -> 42.; + fn lgamma(x: f64) -> f64: (42.) -> 42.; + fn lgammaf(x: f32) -> f32: (42.) -> 42.; + + // TODO: different ABI than in C - need a more elaborate wrapper + // fn lgamma_r(x: f64) -> (f64, i32): (42.) -> (42., 42); + // fn lgammaf_r(x: f32) -> (f32, i32): (42.) -> (42., 42); + + fn log(x: f64) -> f64: (42.) -> 42.; + fn logf(x: f32) -> f32: (42.) -> 42.; + fn log10(x: f64) -> f64: (42.) -> 42.; + fn log10f(x: f32) -> f32: (42.) -> 42.; + fn log1p(x: f64) -> f64: (42.) -> 42.; + fn log1pf(x: f32) -> f32: (42.) -> 42.; + fn log2(x: f64) -> f64: (42.) -> 42.; + fn log2f(x: f32) -> f32: (42.) -> 42.; + fn pow(x: f64, y: f64) -> f64: (42., 42.) -> 42.; + fn powf(x: f32, y: f32) -> f32: (42., 42.) -> 42.; + + // FIXME: different ABI than in C - need a more elaborate wrapper + // fn modf(x: f64) -> (f64, f64): (42.) -> (42., 42.); + // fn modff(x: f32) -> (f32, f32): (42.) -> (42., 42.); + // remquo + // remquof + + fn round(x: f64) -> f64: (42.) -> 42.; + fn roundf(x: f32) -> f32: (42.) -> 42.; + fn scalbn(x: f64, n: i32) -> f64: (42., 42) -> 42.; + fn scalbnf(x: f32, n: i32) -> f32: (42., 42) -> 42.; + + // FIXME: different ABI than in C - need a more elaborate wrapper + // fn sincos + // fn sincosf + + // FIXME: missing symbols - fails to link + // fn sin(x: f64) -> f64: (42.) -> 42.; + // fn sinf(x: f32) -> f32: (42.) -> 42.; + + fn sinh(x: f64) -> f64: (42.) -> 42.; + fn sinhf(x: f32) -> f32: (42.) -> 42.; + fn sqrt(x: f64) -> f64: (42.) -> 42.; + fn sqrtf(x: f32) -> f32: (42.) -> 42.; + // FIXME: missing symbols - fails to link + // fn tan(x: f64) -> f64: (42.) -> 42.; + // fn tanf(x: f32) -> f32: (42.) -> 42.; + fn tanh(x: f64) -> f64: (42.) -> 42.; + fn tanhf(x: f32) -> f32: (42.) -> 42.; + // FIXME: missing symbols - fails to link + // fn tgamma(x: f64) -> f64: (42.) -> 42.; + // fn tgammaf(x: f32) -> f32: (42.) -> 42.; + fn trunc(x: f64) -> f64: (42.) -> 42.; + fn truncf(x: f32) -> f32: (42.) -> 42.; +} diff --git a/crates/libm-cdylib/src/macros.rs b/crates/libm-cdylib/src/macros.rs new file mode 100644 index 000000000..4cbc23fdd --- /dev/null +++ b/crates/libm-cdylib/src/macros.rs @@ -0,0 +1,73 @@ +macro_rules! export { + (fn $id:ident ($($arg:ident : $arg_ty:ty),* ) -> $ret_ty:ty: + ($($test_arg:expr),*) -> $test_ret:expr;) => { + #[no_mangle] + pub extern "C" fn $id($($arg: $arg_ty),*) -> $ret_ty { + // This just forwards the call to the appropriate function of the + // libm crate. + #[cfg(not(link_test))] { + libm::$id($($arg),*) + } + // When generating the linking tests, we return a specific incorrect + // value. This lets us tell this libm from the system's one appart: + #[cfg(link_test)] { + // TODO: as part of the rountrip, we probably want to verify + // that the argument values are the unique ones provided. + let _ = libm::$id($($arg),*); + $test_ret as _ + } + } + + #[cfg(test)] + paste::item! { + // These tests check that the library links properly. + #[test] + fn [<$id _link_test>]() { + use crate::test_utils::*; + + // This re-compiles the dynamic library: + compile_cdylib(); + + // Generate a small C program that calls the C API from + // . This program prints the result into an appropriate + // type, that is then printed to stdout. + let (cret_t, c_format_s) + = ctype_and_printf_format_specifier(stringify!($ret_ty)); + let ctest = format!( + r#" + #include + #include + #include + int main() {{ + {cret_t} result = {id}({input}); + fprintf(stdout, "{c_format_s}", result); + return 0; + }} + "#, + id = stringify!($id), + input = [$(stringify!($test_arg)),*].join(","), + cret_t = cret_t, + c_format_s = c_format_s + ); + + let target_dir = target_dir(); + let src_path = target_dir.clone().join(format!("{}.c", stringify!($id))); + let bin_path = target_dir.clone().join(format!("{}", stringify!($id))); + write_to_file(&src_path, &ctest); + + // We now compile the C program into an executable, make sure + // that the libm-cdylib has been generated (and generate it if + // it isn't), and then we run the program, override the libm, + // and verify the result. + compile_file(&src_path, &bin_path); + check(&bin_path, $test_ret as $ret_ty) + } + } + }; + ($(fn $id:ident ($($arg:ident : $arg_ty:ty),* ) -> $ret_ty:ty: + ($($test_arg:expr),*) -> $test_ret:expr;)*) => { + $( + export! { fn $id ($($arg : $arg_ty),* ) -> $ret_ty: ($($test_arg),*) -> $test_ret; } + )* + } +} diff --git a/crates/libm-cdylib/src/test_utils.rs b/crates/libm-cdylib/src/test_utils.rs new file mode 100644 index 000000000..146ebf805 --- /dev/null +++ b/crates/libm-cdylib/src/test_utils.rs @@ -0,0 +1,163 @@ +use std::{fs, io, path::Path, process}; + +/// Writes the `content` string to a file at `path`. +pub(crate) fn write_to_file(path: &Path, content: &str) { + use io::Write; + let mut file = fs::File::create(&path).unwrap(); + write!(file, "{}", content).unwrap(); +} + +/// Compiles the libm-cdylib library as a C library. +/// +/// This just compiles it once, all other times it just +/// succeeds. We compile it with --cfg link_test to +/// enable the tests. +pub(crate) fn compile_cdylib() { + let mut cmd = process::Command::new("cargo"); + cmd.arg("build"); + if cfg!(release_profile) { + cmd.arg("--release"); + } + cmd.env("RUSTFLAGS", "--cfg=link_test"); + handle_err("lib_build", &cmd.output().unwrap()); +} + +/// Compiles the test C program with source at `src_path` into +/// an executable at `bin_path`. +pub(crate) fn compile_file(src_path: &Path, bin_path: &Path) { + let cc = if std::env::var("CC").is_ok() { + std::env::var("CC").unwrap().to_string() + } else if cfg!(target_os = "linux") { + "gcc".to_string() + } else if cfg!(target_os = "macos") { + "clang".to_string() + } else { + panic!("unknown platform - Ccompiler not found") + }; + let mut cmd = process::Command::new(&cc); + // We disable the usage of builtin functions, e.g., from libm. + // This should ideally produce a link failure if libm is not dynamically + // linked. + // + // On MacOSX libSystem is linked (for printf, etc.) and it links libSystem_m + // transitively, so this doesn't help =/ + cmd.arg("-fno-builtin") + .arg("-o") + .arg(bin_path) + .arg(src_path); + + // Link our libm + let lib_path = cdylib_dir(); + cmd.arg(format!("-L{}", lib_path.display())); + cmd.arg("-llibm"); + + handle_err( + &format!("compile file: {}", src_path.display()), + &cmd.output().unwrap(), + ); +} + +/// Run the program and verify that it prints the expected value. +pub(crate) fn check(path: &Path, expected: T) +where + T: PartialEq + std::fmt::Debug + std::str::FromStr, + ::Err: std::fmt::Debug, +{ + let mut cmd = process::Command::new(path); + + if cfg!(target_os = "linux") { + let ld_library_path = std::env::var("LD_LIBRARY_PATH").unwrap_or_default(); + let ld_library_path = format!("{}:{}", cdylib_dir().display(), ld_library_path); + cmd.env("LD_LIBRARY_PATH", ld_library_path); + } + + // Run the binary: + let output = cmd.output().unwrap(); + handle_err(&format!("run file: {}", path.display()), &output); + // Parse the result: + let result = String::from_utf8(output.stdout.clone()) + .unwrap() + .parse::(); + + if result.is_err() { + panic!(format_output("check (parse failure)", &output)); + } + // Check the result: + let result = result.unwrap(); + assert_eq!(result, expected, "{}", format_output("check", &output)); +} + +pub(crate) fn handle_err(step: &str, output: &process::Output) { + if !output.status.success() { + panic!("{}", format_output(step, output)); + } +} + +pub(crate) fn format_output( + step: &str, + process::Output { + status, + stdout, + stderr, + }: &process::Output, +) -> String { + let mut s = format!("\nFAILED[{}]: exit code {:?}\n", step, status.code()); + s += &format!( + "FAILED[{}]: stdout:\n\n{}\n\n", + step, + String::from_utf8(stdout.to_vec()).unwrap() + ); + s += &format!( + "FAILED[{}]: stderr:\n\n{}\n\n", + step, + String::from_utf8(stderr.to_vec()).unwrap() + ); + s +} + +/// For a given Rust type `x`, this prints the name of the type in C, +/// as well as the printf format specifier used to print values of that type. +pub(crate) fn ctype_and_printf_format_specifier(x: &str) -> (&str, &str) { + match x { + // Note: fprintf has no format specifier for floats, `%f`, converts + // floats into a double, and prints that. + // + // For the linking tests, precision doesn't really matter. The only + // thing that's tested is whether our implementation was properly called + // or not. This is done by making our functions return an incorrect + // magic value, different from the correct result. So as long as this is + // precise enough for us to be able to parse `42.0` from stdout as + // 42_f32/f64, everything works. + "f32" => ("float", "%f"), + "f64" => ("double", "%f"), + "i32" => ("int32_t", "%d"), + _ => panic!("unknown type: {}", x), + } +} + +pub(crate) fn target_dir() -> std::path::PathBuf { + if let Ok(dir) = std::env::var("CARGO_TARGET_DIR") { + std::path::PathBuf::from(&dir) + } else { + Path::new("../../target").into() + } +} + +pub(crate) fn cdylib_dir() -> std::path::PathBuf { + target_dir().join(if cfg!(release_profile) { + "release" + } else { + "debug" + }) +} + +pub(crate) fn cdylib_path() -> std::path::PathBuf { + let libm_path = cdylib_dir(); + if cfg!(target_os = "macos") { + libm_path.join("liblibm.dylib") + } else if cfg!(target_os = "linux") { + libm_path.join("liblibm.so") + } else { + panic!("unsupported target_os") + } +}