Skip to content

RFC: naming groups of configuration with cfg_alias #3804

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions text/3804-cfg-alias.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
- Feature Name: `cfg_alias`
- Start Date: 2025-04-23
- RFC PR: [rust-lang/rfcs#3804](https://github.com/rust-lang/rfcs/pull/3804)
- Rust Issue:
[rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000)

# Summary

[summary]: #summary

This RFC introduces a way to name configuration predicates for easy reuse
throughout a crate.

```rust
#![cfg_alias(
x86_linux,
all(any(target_arch = "x86", target_arch = "x86_64"), target_os = "linux")
)]

#[cfg(x86_linux)]
fn foo() { /* ... */ }

#[cfg(not(x86_linux))]
fn foo() { /* ... */ }
```

# Motivation

[motivation]: #motivation

It is very common that the same `#[cfg(...)]` options need to be repeated in
multiple places. Often this is because a `cfg(...)` needs to be matched with a
`cfg(not(...))`, or because code cannot easily be reorganized to group all code
for a specific `cfg` into a module. The solution is usually to copy a `#[cfg]`
group around, which is error-prone and noisy.

Adding aliases to config predicates reduces the amount of code that needs to be
duplicated, and giving it a name provides an easy way to show what a group of
configuration is intended to represent.

Something to this effect can be done using build scripts. This requires reading
various Cargo environment variables and potentially doing string manipulation
(for splitting target features), so it is often inconvenient enough to not be
worth doing. Allowing aliases to be defined within the crate and with the same
syntax as the `cfg` itself makes this much easier.

# Guide-level explanation

[guide-level-explanation]: #guide-level-explanation

There is a new crate-level attribute that takes a name and a `cfg` predicate:

```rust
#![cfg_alias(some_alias, predicate)]
```

`predicate` can be anything that usually works within `#[cfg(...)]`, including
`all`, `any`, and `not`.
Comment on lines +72 to +73
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd probably drop ", including..." here as it makes it more rather than less ambiguous. Alternatively, it'd be OK to say e.g. "including (but not limited to) combining operators such as all, any, and not.


Once an alias is defined, `name` can be used as if it had been passed via
`--cfg`:

```rust
#[cfg(some_alias)]
struct Foo { /* ... */ }

#[cfg(not(some_alias))]
struct Foo { /* ... */ }

#[cfg(all(some_alias, target_os = "linux"))]
fn bar() { /* ... */ }
```
Comment on lines +75 to +87
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add an example of an alias being used within an alias to make clear that that should work.

The example in https://crates.io/crates/cfg_aliases provides a good motivating case. Might also be good to pull ideas from that readme for the Summary section


# Reference-level explanation

[reference-level-explanation]: #reference-level-explanation

The new crate-level attribute is introduced:

```text
CfgAliasAttribute:
cfg_alias(IDENTIFIER, ConfigurationPredicate)
```

The identifier is added to the `cfg` namespace. It must not conflict with any
builtin configuration names, or with those passed via `--cfg`.

Once defined, the alias can be used as a regular predicate.

The alias is only usable after it has been defined. For example, the following
will emit an unknown configuration lint:

```rust
#![cfg_attr(some_alias, some_attribute)]
// warning: unexpected_cfgs
//
// The lint could mention that `some_alias` was found in the
// crate but is not available here.

#![cfg_alias(some_alias, true)]
```

RFC Question:

Two ways to implement this are with (1) near-literal substitution, or (2)
checking whether the alias should be set or not at the time it is defined. Is
there any user-visible behavior that would make us need to specify one or the
other?

If we go with the first option, we should limit to a single expansion to avoid
recursing (as is done for `#define` in C).

# Drawbacks

[drawbacks]: #drawbacks

- This does not support more general attribute aliases, such as
`#![alias(foo = derive(Clone, Copy, Debug, Default)`. This seems better suited
for something like `declarative_attribute_macros` in [RFC3697].

[RFC3697]: https://github.com/rust-lang/rfcs/pull/3697

# Rationale and alternatives
Copy link
Member

@tmandry tmandry Apr 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inspired by the above, one alternative that comes to mind is declarative attribute macros that do the cfg matching for you. I think actually you have to declare two macros, one when the cfg you want is true and one when it is false, so that's a major drawback because it would require repeating the same clause twice.

However, attribute macros (possibly in combination with this feature) would allow a crate to "export" an alias.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great point, I'll mention that. One other downside is that with a specific set of config tied to an attribute macro, it wouldn't be easily possible to combine with other config in all or any (could probably be done with the attribute macro's parameters).

Exporting would be quite convenient at times.

Copy link
Member

@tmandry tmandry Apr 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point that the macro approach doesn't compose all that well. I wish there was an obvious way to support exporting these.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a more radical design, it might be possible to treat aliases as effectively a new kind of macro; something that shares the macro namespace but only expands within cfg. I think this would mean the new macro scoping can be used, so pub use some_alias can make an alias crate-public for another crate to import with use crate_with_alias::some_alias.

It sounds borderline too complex for an otherwise pretty simple feature, but being able to do that could be a nice help if public macros expand to code that contains #[cfg(...)].

With that, it would almost be possible to define the builtin cfg(windows)/cfg(unix) as something like cfg_alias(windows = target_os = "windows") in the prelude (not that we'd have any reason to actually do that).

Copy link

@mejrs mejrs Apr 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect some kind of way to export an alias. I'd be disappointed with a design that wouldn't permit it.

Anyway, your comment about macros made me think of something like...

// library

#[macro_export]
macro_rules! has_atomics {
    () =>  { any(target = a, target = b, .. ) }
}

// crate
#[cfg(has_atomics!())]
fn blah(){}

That has the advantage of not needing a new kind of attribute or new syntax to define an alias. It also makes it obvious when an alias is being used.

Moreover this syntax implies that you can pass arguments.

Let me give an example where this would be useful to me personally. The Python C api has different ABI guarantees. Take PyObject_Vectorcall for example. This function was added in Python 3.9, but only in 3.12 it was added to the Stable ABI.

That means I define the bindings as:

extern "C" {
    #[cfg(any(Py_3_9, all(Py_3_12, not(Py_LIMITED_API))))]
    pub fn PyObject_Vectorcall(..) -> ...

This would be a lot simpler if the syntax is macro-like:

macro_rules! limited {
    ($added_in:ident) =>  { all($added_in, not(Py_LIMITED_API)) }
    ($added_in:ident, $stable_in:ident) =>  { any($stable_in, all($added_in, not(Py_LIMITED_API))) }
}

#[cfg(limited!(Py_3_9, Py_3_12))]
// ...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do think @mejrs that this would be quite nice. I suspect it might be difficult to implement, though. Maybe what's needed is an experiment.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for that.


[rationale-and-alternatives]: #rationale-and-alternatives

- The syntax `cfg_alias(name, predicate)` was chosen for similarity with
`cfg_attr(predicate, attributes)`. Alternatives include:
- `cfg_alias(name = predicate)`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer the = form. cfg_attr doesn't seem like a compelling precedent when the predicate goes first with cfg_attr and second with cfg_alias.

I also happen to think the cfg_attr syntax is hard to read, we should avoid replicating that :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cfg_attr is definitely not very nice to read or write (though, I don't really know what would have been better here). Reading through it more, I am also starting to prefer = but one concern is that = in config is used for something like equality rather than assignment. Should there be any concern about that here?

I know that also came up in discussion at #3796

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have updated the syntax to use =

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One other downside: with =, it looks a bit weird when aliasing a single = option

#![cfg_alias(alias = target_os = "linux")]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that is a negative. I'll retract my preference then. I'm not crazy about the comma but it's closest to what we have..

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could choose to require parens there.

- It may be possible to have `#[cfg_alias(...)]` work as an outer macro and only
apply to a specific scope. This likely is not worth the complexity.
Comment on lines +190 to +191
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- It may be possible to have `#[cfg_alias(...)]` work as an outer macro and only
apply to a specific scope. This likely is not worth the complexity.

Since this has now been added to the RFC, this alternative can be removed.


# Prior art

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cfg_aliases crate could be mentioned. https://crates.io/crates/cfg_aliases

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only have limited knowledge of the cfg_aliases crate implementation, but worked on the crate which the cfg_alias macro was based upon initially, and testing cfg_aliases it appears to suffer from similar limitations in it's cfg parsing.

In particular it has difficulty with combinations of nested and sequential vararg cfgs such as not(any(a,b), all(c,d)). As well as requiring build.rs.

I feel like both of these are good reasons for cargo not to defer to doing it via a crate, even if the crate is good enough to be useful for most purposes.


[prior-art]: #prior-art

In C it is possible to modify the define map in source:

```c
# if (defined(__x86_64__) || defined(__i386__)) && defined(__SSE2__)
#define X86_SSE2
#endif

#ifdef X86_SSE2
// ...
#endif
```

# Unresolved questions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we decide that the complexity of allowing this at a module level is too high, that is, only allowing it as a crate attribute, then I would argue that the feature would be better suited to only being accessible in Cargo / --cfg-alias.


[unresolved-questions]: #unresolved-questions

Questions to resolve before this RFC could merge:

- Which syntax should be used?
- Substitution vs. evaluation at define time (the question under the
reference-level explanation)
Comment on lines +216 to +217
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Substitution vs. evaluation at define time (the question under the
reference-level explanation)
- Substitution vs. evaluation at define time (the question under the
reference-level explanation).


# Future possibilities

[future-possibilities]: #future-possibilities

- A `--cfg-alias` CLI option would provide a way for Cargo to interact with this
feature, such as defining config aliases in the workspace `Cargo.toml` for
reuse in multiple crates.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
reuse in multiple crates.
reuse in multiple crates.
- We could add a visibility to the syntax, allowing a crate to export a cfg alias for use by other crates.

Comment on lines +223 to +225
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I strongly suspect that having this feature in Cargo will be desired, and that having it there would solve 90% of the use-case for this feature.

Implementation-wise, I also suspect that doing it would be possible and fairly simple in Cargo today by parsing the cfg itself (it already understands cfgs as part of dependency resolving), and then passing extra --cfgs / --check-cfgs to the compiler.

So maybe it would be valuable to implement (or at least stabilize) the ability for doing this in Cargo first, and only later consider making this an attribute in the language itself?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This gets into a weird gray area of Cargo.

Defining --cfgs is a rustc concept that is lower level than Cargo. In fact, there was a proposed [cfg] table for --check-cfg which as rejected for that reason. Instead, cfgs are either defined by the environment or by [features]. We do recognize there is a gap for more --cfg use cases and have done some brainstorming on "global features" but more work is needed.

Cargo does allow reading of cfg's through build script environment variables and through target.* tables.

Depending on how you look at it, a --cfg-alias is like --cfg and too low level for Cargo. As people want --cfgs to influence "global features", maybe there is something there that can be designed that can fill both needs. Or --cfg is a helper for #[cfg()]s and would fit similar to build scripts and target.* tables.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As pointed out, there is a build script to emulate cfgs: https://github.com/rust-lang/rfcs/pull/3804/files#r2059296180

With metabuild, we could go a step further in semi-native cargo support. This would allow something like

[[package.build]]
dependency = "cfg_aliases"
cfg = {
  wasm = 'target_arch = "wasm32"',
  android = 'target_os = "android"',
  surfman = 'all(unix, feature = "surfman", not(wasm))'
}

(newlines in the inline table is me being hopeful that TOML 1.1 is finally unblocked)