Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
wants to merge 8 commits into from

Conversation

gnzlbg
Copy link
Contributor

@gnzlbg gnzlbg commented Jul 2, 2019

This PR adds a new crate libm-cdylib that implements the <math.h> API and that is ABI compatible with the platforms libm implementation.

Since libm is usually dynamically linked, this allows replacing the libm implementation of any program in the platform with the Rust implementation, e.g., using LD_PRELOAD and similar APIs.

The library is tested by just linking it to C programs that use <cmath.h> and checking that the library is called and produce expected "unique" results that differ from the system libraries. The C programs are linked to the system library, and then the math library is dynamically overriden.

Right now this works properly on macos, I'll iterate on the linux part of this on CI.

#[cfg(not(test))]
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
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.


#[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.

pub(crate) fn ctype_and_printf_format_specifier(x: &str) -> (&str, &str) {
match x {
"f32" => ("float", "%f"),
"f64" => ("double", "%f"),
Copy link
Contributor

Choose a reason for hiding this comment

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

should this be %lf?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

IIRC, %f prints doubles. There is no format string for printing floats AFAIK - %f converts floats into a double, and prints that.

Copy link
Contributor

Choose a reason for hiding this comment

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

Would it make sense when %f is a double anyway? maybe %.24f for floats %.54f for doubles makes more sense?

Copy link
Contributor

@Schultzer Schultzer Jul 2, 2019

Choose a reason for hiding this comment

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

Or even better maybe just %e

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For the linking tests it doesn't really matter. I'll add a comment about this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So I've added a comment explaining what this does right now.

If we wanted to verify the results of this lib, I think I would prefer doing so as part of the libm-test system. We could dynamically link libm as the system library, and just verify that the results of the C ABI match the results that are produced by the Rust library, or something like that.

The problem which checking the ABI here is that, if for some reason linking failed, or the symbol has a slightly different name, etc. the functions from the system library are invoked. So if we were to just simply verify the results, we can't tell whether its our libm or the system libm that's being invoked.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe there is a way to reliably prevent libm from being linked on Linux, while still being able to link, e.g., fprintf and other parts of the standard library. On MacOSX, this is IMO not worth it - everything links libSystem.dylib, which always dynamically links libm.

@Schultzer
Copy link
Contributor

Schultzer commented Jul 2, 2019

Is there anyway we can fix the linking issue?

By looking the at the online docs the following functions should be present in the Kernel

  • atan
  • cos, cosf
  • j0, j1, jn

There are no sincos or exp10 functions in the Kernel, they are only present in the accelerate or simd framework as vectorized version.

@gnzlbg
Copy link
Contributor Author

gnzlbg commented Jul 2, 2019

The problem is that some symbols are missing from the cdylib, like _memcpy, _memset, etc. I don't know why, and would prefer to merge this as is, and try to fix each one of those issues in follow up PRs. This issue appears related: rust-lang/rust#44871

cc @alexcrichton


#[cfg(not(test))]
#[lang = "eh_personality"]
extern "C" fn eh_personality() {}
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?

#![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.

// 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.

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.

@@ -0,0 +1,143 @@
use std::{fs, io, path::Path, process};
Copy link
Member

Choose a reason for hiding this comment

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

I personally feel like this file may be a bit over-the-top when testing, I'm not sure it's really all that necessary to be invoking Cargo from this crate's own test suite to test various C things?

If it's really this extensive then I would prefer this not live in this repository to help keep this repository manageable and not pull in a whole set of new maintenance.

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.

It isn't expensive on my 8 year old system - cargo is invoked on every test, but the first invocation "locks", and once the first invocation completes, all of them just return "success - the library was already built".

I can use a lazy_static! to just call cargo once, but it felt like unnecessary complexity.

@gnzlbg
Copy link
Contributor Author

gnzlbg commented Jul 3, 2019

So I've cleaned this up, and CI is green. I've added two MacOSX build jobs, and a nightly x86_64-linux build job. For the time being, the tests are only run there.

This is sub-optimal. Ideally, we would have a libm library, that exposes an extern "C" API, and a libm-rs library, that wraps that API in the F32Ext/F64Ext traits. We would then only exhaustively test libm and only have minimal tests in libm-rs. When compiling libm as a cdylib, we would use panic=abort, while we compiling it as a rlib, we would use panic=unwind (to allow linking it against both abort/unwind code).

But these are architectural decision that have to be made depending on the goals of the library, so I've opened #195 to discuss that first.

@alexcrichton
Copy link
Member

It seems like this is intended to build on #198 so I'm going to hold off on reviewing/merging until that's landed.

@gnzlbg gnzlbg closed this Jul 12, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants