Awesome FOSS Logo
Discover awesome open source software
Launched 🚀🧑‍🚀

Getting Features to Show Up in Your Rust Docs

Categories
Rust logo

tl;dr - Update your Cargo.toml and make sure doc_auto_cfg is turned on in lib.rs

How do you properly show feature specific functionality (and mark them with the required feature) for library crates in 2024?

First, you need to update your Cargo.toml:

[package.metadata.docs.rs]
all-features = true

If you don’t want all features to be enabled (if you want some features and related code to be legitimately hidden from docs), you can use specific features:

[package.metadata.docs.rs]
features = [ "one", "three" ]

In addition to the above, you have to add the following to your src/lib.rs:

#![cfg_attr(docsrs, feature(doc_auto_cfg))]

Let’s break down what this does real quick:

  • cfg_attr is a feature that let’s you optionally enable an attribute if a cfg flag is set is set
  • docrs is an cfg attribute that is set when docs.rs tries to build your documentation
  • doc_auto_cfg is a nightly feature (for now) that automatically adds #[doc(cfg(feature = "..."))] to code that requires a given feature

The combination of these produces docs that expose feature-gated functions/traits/structs/etc and notes which features they are gated by.

Rust’s feature support is awesome

Features are a really amazing feature of Rust. It’s not like Rust invented feature gating, feature flagging or conditional compilation, but I’ve rarely seen the feature be so ergonomic in other languages – and I’ve used a bunch of other languages.

Use-case: Segmenting your libraries and it’s functionality

In other languages what would you do if you had a feature that you only wanted to use if the user opted into it?

Most of the time this just isn’t done. The most obvious solution is a if !someFeatureEnabled() { ... } block in all the code that supposedly used that feature, and some language will actually give you a proper feature system and/or it’s terrible cousin, a clunky macro system.

Rust provides some of the most ergonomic support for doing this that I’ve seen. Let’s say your library that prints a message but sometimes you want that message to have emojis, but only if the person who is using your library loves emojis.

First you add the feature to your Cargo.toml:

[features]
emojify = []

And in your code, you can write a function that uses the feature

fn greet() -> String {
    #[cfg(feature = "emojify")]
    let s = String::from("Hey there 👋 ")

    #[cfg(not(feature = "emojify"))]
    let s = String::from("Hello, how are you?")

    s
}

Of course in most normal code you might choose to pass through some sort of configuration value to greet() or centralize the creation of these messages, but for really cross-cutting concerns that show up everywhere and are somewhat “global”, this is a very ergonomic and simple-to-grok way to solve the problem!

When your user pulls in your library in their own Cargo.toml, they can specify the feature:

your-awesome-lib = { version = "x.x.x", features = [ "emojify" ] }

You can also use cargo add:

cargo add your-awesome-lib --features=emojify

Of course, you should make sure to update your documentation to let people know the how and why of the feature.

Use-case: Cross-platform support

When writing interpreted languages like NodeJS/Python/Ruby, cross-platform support is essentially “guaranteed”. You write high level code and some interpreter does the hard work of calling the appropriate platform-specific syscalls and doing the work for you.

Even languages like Haskell don’t have a super ergonomic solution to this – you essentially end up only being able to put the if at the module level only (or worse, using CPP style ifdefs).

Rust also does that (it’s standard library has cross-platform APIs), but it gives you a level of control and makes it pretty easy to change up what you do, per-platform:

fn vibecheck() -> String {
    #[cfg(all(not(unix), not(windows)))]
    let platform = "???";

    #[cfg(unix)]
    let platform = "unix";

    #[cfg(windows)]
    let platform = "windows";

    format!("You're on [{platform}], consider Arch Linux.")
}

Isn’t that simple? You can now build functions that are platform aware quite easily, and let other functionality ignore this facet at higher levels of abstraction.

Advanced usage: You can set your own cfgs!

One of the cool things that Rust enables that you may not have thought about is that you can introduce your own cfg tags.

All you need is some extra code in your library’s build.rs script.

The simplest example is creating a simple boolean cfg that is either there or it’s not:

fn main() {
    // Declare your intent to use a custom cfg (new syntax in Rust 1.80)
    println!("cargo::rustc-check-cfg=cfg(it_works)");

    // Actually set the cfg value as present
    println!("cargo::rustc-cfg=it_works");
}

You can also create cfgs that have a range of values:

fn main() -> {
    // Declare your intent to use a custom cfg (new syntax in Rust 1.80)
    println!(r#"cargo::rustc-check-cfg=cfg(it_works, values("yep", "nope"))"#);

    // Actually set the cfg to a value
    //
    // In a realistic codebase you'd probably do more to figure out this value below
    let it_works = "yep";
    println!(r#"cargo::rustc-cfg=it_works="{it_works}""#);
}

In your code, you can now use the config as if it was built into the language and Rust guarantees that it_works is either "yep" or "nope":

src/lib.rs

#[cfg(it_works = "yep")]
fn do_it() -> &'static str {
    "JUST DO IT!"
}

#[cfg(it_works = "nope")]
fn do_it() -> &'static str {
    "JUST DON'T IT!"
}

This is a silly example but you can use this for a LOT of different things – you can:

  • Detect if software is installed on the builder’s machine at runtime (ex. does the build machine have docker installed?)
  • Check for access to the internet (this has come up at $DAYJOB)