You don’t want to compile what you don’t use, and you don’t want to use what you don’t need - let’s talk about cfg_attr.

In Rust, conditional compilation is a powerful tool that lets you selectively compile pieces of code based on compilation context. One often overlooked gem in this toolset is the cfg_attr attribute, which provides conditional application of other attributes. In this blog post, I’ll try and show how one can utilize cfg_attr on structs to enable faster compilation times in non-test environments and ensuring redundant code isn’t shipped to customers.

The Problem

When writing Rust applications, it’s common to derive various traits on structs for different tasks. For instance, the serde library, which is used for serialization and deserialization, provides the Serialize and Deserialize traits.

In some scenarios, we might want to serialize certain structs for testing purposes but avoid including this serialization logic in our final build. The reasons could be:

  • Speeding up compilation time.
  • Reducing the final binary size.
  • Security concerns - serialization logic could potentially be insecure, and if we don’t need it in our final customer facing build, we should avoid including it.

Introducing cfg_attr

This is where cfg_attr comes into play. The cfg_attr attribute conditionally applies another attribute based on a given configuration predicate. The general syntax is:

#[cfg_attr(condition, attribute)]

Using this, we can apply the serde::Serialize trait only when we’re compiling for tests:

#[cfg_attr(test, derive(serde::Serialize))]
struct MyStruct {
    field1: u32,
    field2: String,
}

Here, the serde::Serialize trait is derived for MyStruct only if the code is being compiled with the test configuration enabled. Let’s see that in action:

fn main() {
    let obj = MyStruct { 
        field1: 42, 
        field2: "Hello, world!".to_string() 
    };

    println!("{}", serde_json::json!(obj)); // <- ❌ This doesn't compile!
}

#[cfg(test)]
mod tests {
    #[test]
    fn test() {
        let obj = MyStruct { 
            field1: 42, 
            field2: "Hello, test!".to_string() 
        };

        println!("{}", serde_json::json!(obj)); // <- ✅ This works!
    }
}

As expected, the code doesn’t compile in the main function, but it does in the test function.

It means that we successfully applied the serde::Serialize trait only when compiling for tests. When building for production, the compiler won’t even bother compiling the serialization code, and will save us some precious time.

Note about integration tests

This solution won’t work for Rust’s integration tests and the issue it tracked here. A workaround some github user found to be working nicely is create an automatic feature flag for tests (both unit and integration) and use that to conditionally derive the trait.

In Cargo.toml:

[dev-dependencies]
# Replace your_package_name with the built package name
your_package_name = { path = ".", features = ["automatic_test_feature"] }

Then, you’ll be able to use the feature flag in your code:

#[cfg_attr(feature = "automatic_test_feature", derive(serde::Serialize))]
struct MyStruct {
    field1: u32,
    field2: String,
}

Conclusion

Rust’s cfg_attr provides an elegant and powerful solution to conditionally derive traits based on compile-time configurations. By being judicious in its use, developers can optimize compile times, reduce binary size, and enhance the security of their applications.

This is just one of the myriad of ways in which Rust’s type system and compiler attributes allow for safe and efficient code.

Until next time.