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

[WIP] Provide a cdylib with the libm C ABI #192

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ checked = []
members = [
"crates/compiler-builtins-smoke-test",
"crates/libm-bench",
"crates/libm-cdylib",
]

[dev-dependencies]
Expand Down
22 changes: 21 additions & 1 deletion azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
28 changes: 24 additions & 4 deletions ci/run.sh
Original file line number Diff line number Diff line change
@@ -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
20 changes: 20 additions & 0 deletions crates/libm-cdylib/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "libm-cdylib"
version = "0.1.0"
authors = ["Gonzalo Brito Gadeschi <gonzalobg88@gmail.com>"]
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"
17 changes: 17 additions & 0 deletions crates/libm-cdylib/build.rs
Original file line number Diff line number Diff line change
@@ -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");
}
}
163 changes: 163 additions & 0 deletions crates/libm-cdylib/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this do extern crate libm instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would pull in std, see below.


#[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() }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: I should probably call the C abort function here.

}

// TODO: there has to be a way to avoid this
#[cfg(not(test))]
#[lang = "eh_personality"]
extern "C" fn eh_personality() {}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There has to be a better way that avoids this, but I exhausted all my google-fu trying to find it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has to do with libcore itself being built with panic=unwind, but why not just link to std and have it provide these?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because libstd would pull the system's libm.


// 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.;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we really want to maintain a cdylib in this repository then I would prefer that this list were automatically inferred instead of duplicated with the definitions in the main source code. If the duplication is going to happen here then I would prefer that this cdylib is provided in a different repository than this one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still going to have to generate some of these by hand though because of the API mismatches between this libm, and C's libm.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah even the original test harness does that, but it's not as explicit why it does it, I had to look up the musl code to understand what was going on in the test harness.

}
73 changes: 73 additions & 0 deletions crates/libm-cdylib/src/macros.rs
Original file line number Diff line number Diff line change
@@ -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))] {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a pretty complicated macro to test what already has a pretty complicated of tests seems like overkill? Why does the cdylib itself need to be tested when it's all already tested elsewhere?

Copy link
Contributor Author

@gnzlbg gnzlbg Jul 2, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This tests that the C ABI we provide is compatible with <math.h>, which is what allows this library to be used from C.

We currently don't test it, and in fact, libm currently doesn't provide a C compatible ABI, which is why some of the functions in the lib.rs are commented out.

Simplest example is the libm functions returning tuples by value (e.g. (i32, f32)). The libm C ABI doesn't do that, and takes pointer arguments instead that are used as "out" parameters. So the cdylib is going to have to workaround those manually.

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
// <math.h>. 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 <math.h>
#include <stdio.h>
#include <stdint.h>
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; }
)*
}
}
Loading