[back]
Conditional Compilation Across Crates
modified: 2015-03-22 04:22:15

I ran into a situation where I needed to do conditional compilation based on what a crate contained or supported. My project uses the same crate name but would produce different crates depending on the system, target(cpu), and board. So some would offer more features. In my quest to solve this problem I found an interesting way to do it with LTO (link-time optimization).

Take for example the crate Grape:

    pub fn havethis() -> bool {
        true
    }

    pub fn thisfoo(a: uint) -> uint {
        a + 3
    }

Now, take Apple our usage crate:

    extern crate grape;

    fn main() {
        let a: uint;

        if grape::havethis() {
            a = grape::thisfoo(5)
        } else {
            a = 0;
        }

        println!("{}", a);
    }

The goal is to make this conditional check free where you incur no penalty for checking and writing code using the feature. Your first assumption would be to use the #[cfg(x)] directive, but this for one is only works for functions and structures and can not be used, currently, for code blocks. Since you can not use it for code blocks you will end up making functions and multiple versions which essentially lands you into something looking like the above, but to make matters worse that #[cfg(x)] will not work across crates. For example #[cfg(grape::havethis)] just will not work.

The above works but there is a problem. The only optimization done by rustc by default is per crate meaning it will emit the call to grape::havethis() and the supporting code inside the block which means you have a cost and depending on the circumstances this would be expensive. This is because the compiler can not be sure what grape::havethis will return therefore it must emit the code in the block in the event the check becomes true.

You can however enable LTO (link-time optimization) with the argument -C lto passed to rustc. From my limited experiments it seems that LLVM (which I presume is doing the optimization) is capable of optimizing from such a simple function as grape::hasthis and will actually omit the code block making the check essentially free. I am not exactly sure how LTO works, but apparently it can determine that grape::havethis will always return true or false and therefore during compile time of the crate Apple determine that the code block is dead and omits it from the emitted machine code.

However, I did notice a very significant increase in compile time with LTO enabled!

There is still one problem. You still have to provide a symbol for the functions even if grape::hasthis returned false. You can however just make the implemented versions panic when it is not supported. This will allow you to implement conditional compilation across crates based on the existence of crate features with out incurring run-time cost, but at the problem of having to write extra code to implement unsupported functions with a panic, loop, or some form of errored return.

Now, the next problem you will encounter is if say you wanted the compilation to just fail if a feature was not provide. This is going to be difficult because we are originally relying on optimization which happens in the later stage of compilation therefore there is no way for rustc to detect this which was our original problem. So instead we are left with having to make the image output something at runtime saying that it can not run because it is missing feature foo because hasfoo returned false.

So we need a better solution and obviously the only way to resolve this is by two methods. Either we include some external metadata and have our build system determine using this metadata that a required feature is not present and fail building, or we output something special into the final binary that our build system can detect and alert the user about. Each method has its cons and pros but since having your build system do this is going to be outside the scope of this paper let us explore the method of detecting something in the image once it is built, which could be fun too!

    #![feature(asm)]
    #![feature(macro_rules)]

    extern crate grape;

    fn dopanic(msg: & 'static str) {
        panic!(msg);
    }

    macro_rules! featurepanic(
        ($msg:expr) => (
            unsafe {
                dopanic($msg);
                asm!(concat!(concat!("
                    .long 12345678
                    .long 87654321
                    .ascii \"", $msg),"\"
                    .byte 0"));
            }
        )
    )

    fn main() {
        if !grape::hasfoo() {
            featurepanic!("missing feature grape::foo");
        }
    }

You can then search the image for the signature and extract the message!

Now, this has the potential to be a problematic. You could likely make it better possibly by using a better signature which reduces the possibility of a clash. Also you could verify that the message uses valid characters and has a valid zero byte with-in a certain size. These extra steps will make the likely hook of a clash very unlikely, but possible. If you could implement some type of hash you could make it even better and less likely to clash with something produced by the actual machine code and data of the image. Also, you have a dependancy on a feature of not only the Rust language but also of LLVM because at any time things could change and most likely there would be a compatible solution you have to be aware that this is not pure Rust and there is less of a contract that things like this will not break.