Maurice Kayser
Maurice Kayser

Reputation: 805

Can I create my own conditional compilation attributes?

There are several ways of doing something in my crate, some result in fast execution, some in low binary size, some have other advantages, so I provide the user interfaces to all of them. Unused functions will be optimized away by the compiler. Internal functions in my crate have to use these interfaces as well, and I would like them to respect the user choice at compile time.

There are conditional compilation attributes like target_os, which store a value like linux or windows. How can I create such an attribute, for example prefer_method, so I and the user can use it somewhat like in the following code snippets?

My crate:

#[cfg(not(any(
    not(prefer_method),
    prefer_method = "fast",
    prefer_method = "small"
)))]
compile_error("invalid `prefer_method` value");

pub fn bla() {
    #[cfg(prefer_method = "fast")]
    foo_fast();

    #[cfg(prefer_method = "small")]
    foo_small();

    #[cfg(not(prefer_method))]
    foo_default();
}

pub fn foo_fast() {
    // Fast execution.
}

pub fn foo_small() {
    // Small binary file.
}

pub fn foo_default() {
    // Medium size, medium fast.
}

The user crate:

#[prefer_method = "small"]
extern crate my_crate;

fn f() {
    // Uses the `foo_small` function, the other `foo_*` functions will not end up in the binary.
    my_crate::bla();

    // But the user can also call any function, which of course will also end up in the binary.
    my_crate::foo_default();
}

I know there are --cfg attributes, but AFAIK these only represent boolean flags, not enumeration values, which allow setting multiple flags when only one enumeration value is valid.

Upvotes: 4

Views: 2659

Answers (1)

Andrew Gaspar
Andrew Gaspar

Reputation: 548

Firstly, the --cfg flag supports key-value pairs using the syntax --cfg 'prefer_method="fast"'. This will allow you to write code like:

#[cfg(prefer_method = "fast")]
fn foo_fast() { }

You can also set these cfg options from a build script. For example:

// build.rs
fn main() {
    println!("cargo:rustc-cfg=prefer_method=\"method_a\"");
}
// src/main.rs
#[cfg(prefer_method = "method_a")]
fn main() {
    println!("It's A");
}

#[cfg(prefer_method = "method_b")]
fn main() {
    println!("It's B");
}

#[cfg(not(any(prefer_method = "method_a", prefer_method = "method_b")))]
fn main() {
    println!("No preferred method");
}

The above code will result in an executable that prints "It's A".

There's no syntax like the one you suggest to specify cfg settings. The best thing to expose these options to your crates' users is through Cargo features.

For example:

# Library Cargo.toml
# ...
[features]
method_a = []
method_b = []
// build.rs
fn main() {
    // prefer method A if both method A and B are selected
    if cfg!(feature = "method_a") {
        println!("cargo:rustc-cfg=prefer_method=\"method_a\"");
    } else if cfg!(feature = "method_b") {
        println!("cargo:rustc-cfg=prefer_method=\"method_b\"");
    }
}
# User Cargo.toml
# ...
[dependencies.my_crate]
version = "..."
features = ["method_a"]

However, in this case, I'd recommend just using the Cargo features directly in your code (i.e. #[cfg(feature = "fast")]) rather than adding the build script since there's a one-to-one correspondence between the cargo feature and the rustc-cfg being added.

Upvotes: 6

Related Questions