8000 GitHub - zesterer/typed_cfg: A type-checked alternative to cfg(feature)
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

zesterer/typed_cfg

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

typed_cfg

A statically-typed and exhaustively-checked alternative to cfg(feature).

The problem

Let's say you're writing a library. It has two features, foo and bar.

Some functions are only enabled by foo. Some functions are only enabled with bar.

#[cfg(feature = "foo")]
fn frobnicate() {
    ...
}

#[cfg(feature = "bar")]
fn barnicate() {
    ...
}

Later in development, you make a mistake. You accidentally call a bar-only function from a foo-only function. That's a problem! It means that if a user only wants to enable foo, they end up with a compilation error unless they enable bar too - that's a semver-breaking change!

#[cfg(feature = "foo")]
fn frobnicate() {
    ...
    barnicate(); // Uh oh!
    ...
}

How do you test for this in CI? Most projects do a compilation check with no features enabled and all features enabled, but the only way to catch bugs like this is to do an exhaustive check through every combination of features. With just a handful of features, you already start needing hundreds of compilation checks - clearly, this isn't viable!

The solution

typed_cfg provides a solution: what if Rust's type system could be used to perform these checks?

Here's how it works:

use typed_cfg::*;

// First, we list the features that our crate supports
cfgs! { feature = { "foo", "bar" } }

// Next, we express our feature gates as *trait bounds*

fn frobnicate() where feature: Is<"foo"> {
    ...
}

fn barnicate() where feature: Is<"bar"> {
    ...
}

That's it! Users of your crate can call the functions as normal. Only one change is required in CI: we perform a normal cargo check, but with the CHECK_CFG environment variable set.

CHECK_CFG=1 cargo check

Now, let's see what happens if we accidentally make the mistake we made before:

error[E0277]: Configuration requirements are not always met
  --> src/main.rs:8:5
   |
8  |     barnicate();
   |     ^^^^^^^^^^^ The compile-time condition cfg(feature = "bar") is not always true in this scope
   |
   = help: the trait `typed_cfg::Is<"bar">` is not implemented for `feature`
   = note: Consider adding a `where feature: Is<"bar">` bound to ensure the caller respects the required configuration
note: required by a bound in `barnicate`
  --> src/main.rs:11:31
   |
11 | fn barnicate() where feature: Is<"bar"> {
   |                               ^^^^^^^^^ required by this bound in `barnicate`

Bingo! The compiler has successfully alerted us to our mistake, and we've avoided breaking our crate's API for downstream users.

Targets and more

typed_cfg doesn't just work with feature flags! Arbitrary cfg keys are also supported, such as target_family:

use typed_cfg::*;

// The `read_at` operation is only supported on POSIX-like operating systems!
fn file_read_at(file: &File, buf: &mut [u8], offset: u64) -> std::io::Result<usize>
    where target_family: Is<"unix">
{
    #[cfg(target_family = "unix")]
    std::os::unix::fs::FileExt::read_at(file, buf, offset)

    // On non-POSIX platforms, the function isn't even callable!
    #[cfg(not(target_family = "unix"))]
    unreachable!()
}

Using typed_cfg

Unfortunately, typed_cfg does rely on a (currently) nightly-only Rust feature, trivial_bounds.

About

A type-checked alternative to cfg(feature)

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

0