`#![no_alloc]` attribute in 2021 edition?

Just had this random idea that I quite like, so let's see what others think! :grinning:


We have a #![no_std] attribute to opt-out of std. Why not introduce a #![no_alloc] attribute that does the same for alloc? #![no_alloc] would imply #![no_std], so they wouldn't both need to be declared.

One advantage of this is that extern crate alloc; would no longer be needed as an opt in to allocator support, in the same way that extern crate std; is not necessary when using std. The alloc prelude could be stabilized, being implicit when #![no_alloc] is not specified in the same way that use std::prelude::v1::*; is implicit when #![no_std] is not present.

This is obviously a breaking change, as it makes allocator support opt-out rather than opt-in. As such, I am proposing this for inclusion in the 2021 edition. It couldn't have been done for the 2018 edition, as extern crate alloc wasn't stabilized until 1.36. As such, I presume it was never even thought of back then.

I am not particularly familiar with MIR, but I believe that it should be possible to have both existing code and my proposal compile to the same IR, which is what's required for an edition.

4 Likes

yay for more progress on banishing extern crate to ancient history!

2 Likes

My preference is getting rid of #![no_std] and instead have search directory dependencies tracked in Cargo.toml too:

10 Likes

So everyone that wants to use stdlib would have to explicitly say so, if I'm understanding you correctly? I don't fully understand what that issue is tracking and how it relates to this in any way (aside from extern crate alloc not being necessary with this, which would be trivial).

Yes, but maybe with an opt-out of the default so if you simply want to use std nothing changes. So for a pure core-only crate you would simply declare that you only depend on core.

[package]
no-default-deps = true

[dependencies]
core = { search-path = true }

If you want to be optional on alloc then you could declare that in the Cargo.toml too:

[package]
no-default-deps = true

[dependencies]
core = { search-path = true }
alloc = { search-path = true, optional = true }
3 Likes

But #![no_std] does opt-out from both std and alloc. If you need one of those, you opt-in for them using extern crate. You can not use alloc in a no_std crate without it. So I do not quite understand the purpose of #![no_alloc].

I also would prefer if dependency on std and alloc was specified in Cargo.toml. It would also result in removal of explicit std and alloc features so common in no_std-compatible crates (i.e. you would have optional dependencies instead).

4 Likes

I agree more with the proposal of moving built-in dependencies to Cargo.toml. cargo new/init already adds the newest available edition by default. It wouldn't be that difficult to add default dependencies, as well.

It could look like this, then:

[package]
name = …
version = …
authors = [ … ]
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[builtin_dependencies]
std = "*"

[dependencies]

If one only depends on core and alloc, it'd be easy to change it to:

[package]
name = …
version = …
authors = [ … ]
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[builtin_dependencies]
core = "*"
alloc = "*"

[dependencies]

And while we're at it, it'd be nice, if features would also be moved out of lib/main.rs:

[package]
name = …
version = …
authors = [ … ]
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[builtin_dependencies]
std = { version = "*", features = ["min_const_generics", "raw_ref_macros", …] }

[dependencies]

I'd use a separate dependencies section, because those internal crates do not get downloaded from the registry. I think it's confusing to special-case those crates in the same section. Adding a new one clarifies, that they behave differently.

By the way, it doesn't really make sense to add a version, so perhaps having features on the right-hand side might be better, e.g.

[builtin_dependencies]
std = ["min_const_generics", "raw_ref_macros", …]

and

[builtin_dependencies]
core = []
alloc = []
1 Like

(I don't have a super-informed opinion here)

I fear that this pushes us more into the alloc/core/std split world. There's long have been talks about a different world with one crate (std) with feature flags (portability lint is a part of that). I worry (with low-confidence level), that, by reliving the pressure from shipping unified std, we are actively moving from that.

7 Likes

For what it's worth, I would vastly prefer having feature flags on stdlib to enable/disable alloc and std support.

2 Likes

The semantics would definitely change. #![no_std] would imply access to core and alloc but not std, #![no_alloc] would imply access to only core. No attribute would imply access to all three.

Personally I strongly prefer separate crates to a single huge crate with feature flags. To me feature flags are less transparent compared to separate crates. Also separate crates make portability easier (see this proposal). Ideally I would love to see even finer grained split (e.g. separate net and fs crates), so it would be very easy to see which capabilities are used by a crate. It would allow for example embedded project to implement the net core crate and use all crates dependent on it, without bothering with fs and other parts which do not make sense for it. I think had we this split, we would not get the nonsensical implementation of std for wasm32-unknown-unknown.

So in addition to introducing a new attribute you also propose to significantly change semantics of the widely used #![no_std]? It would break a lot of projects during migration without a good enough reason, so sounds like a non-starter to me.

rustfix could trivially replace #![no_std] with #![no_alloc] in an edition upgrade, which would preserve current semantics. A one line change when upgrading editions seems more than reasonable to me.

1 Like

This is a completely different meaning of features, with moving std into being declared like a normal dependency (and with -Zbuild-std support instead of actually using the search-path :thinking:) we would be able to move closer to the world @matklad envisions, where std would use actual cfg features and we could declare something like:

[dependencies]
std.default-features = false
std.features = ["alloc", "thread", "net"]

And that would give an alternative way to do optional-alloc libraries that would avoid having to remember which path to use for which items, everything would remain just a conditionally available item under std:

[features]
alloc = ["std/alloc"]
[dependencies]
std.default-features = false

We already have registry/path/git dependencies all in the same section, and I believe to fully embrace this model it would also need to support build and test dependencies:

[dependencies]
core = ...
[build-dependencies]
std = ...
[test-dependencies]
test = ...
1 Like

There's also an intermediate option with slightly different tradeoffs for no_std cases:

  • a crate can require std explicitly as a dependency with some features;
  • if if does not, std is available implicitly if required by the final crate being built;
  • when some part of a library crate needs std::something, the recommended condition is #[cfg(accessible(::std::something))] as accepted in RFC 2523.
3 Likes

Rather them changing #![no_std], I would suggest renaming the symbols to define what we are using: E.g.

  • #![use_std(none)] to replace #![no_core] (this option would be unstable)
  • #![use_std(core)] to replace current #![no_std]
  • #![use_std(core,alloc)] for your proposed definition of #![no_std]
  • #![use_std(std) for the full std (the default if no use_std is specified)
  • #![use_std(core,alloc,std) for the full std but with all paths available (with a reduced std prelude to eliminate dublications)

Enumerations can also be split into multiple statements, e.g. to add std paths conditionally.

This would mainly control the names, by which the standard libary is exposed and what is minimally required. By default if we decide to keep std split into core/alloc/std, rustc would link the appropriate libaries. If we decide to merge them, it would check for the appropriate configs and redirect core/alloc/std paths to std. A future cargo might introduce a statement to select a specific std-dependency in which case rustc would check if this dependency can satisfy the requirements specified by use_std.

The #![no_core] attribute would be removed and #![no_std] would be a depreciated alias to #![use_std(core)] in the 2021 edition.

5 Likes

This seems reasonable to me. The attribute name would need some hashing out, but it would be a better analogue to crate features, were stdlib to move in that direction in the future.

1 Like

Has anyone considered making the standard library opt-in, so no_std is the default? This has a few advantages:

  • The system is easier to understand, no dependencies are added implicitly
  • People are encouraged to make their crates no_std compatible

cargo fix could apply the needed changes automatically when migrating, and cargo new could automatically add the attribute that enables std support.

Also, the compiler should give nice, actionable error messages when a path starting with std is used but the standard library isn't enabled.

4 Likes

This sounds resonable.

By default rustc would link core (or a minimal configuration version of core/alloc/std merged std) from sysroot. If a dependency std is specified in Cargo.toml, cargo would pass --extern std=somefile.rlib to rustc in order to replace core by std. This std would be the current std or a default/full configuration version of the merged core/alloc/std std. In this case, calls to core would be redirected to std. In theory one could also use a custom version of std.

If one needs alloc, an #![ensure_alloc] attribute would make sure, that alloc is available under the name alloc, either by linking the alloc crate from sys-root or if core/alloc/std get merged, switch core to an alloc enabled versions and alias alloc to core or, if --extern std=somefile.rlib is used, alias alloc to std.