|
|
Subscribe / Log in / New account

The Rust 2024 Edition takes shape

By Daroc Alden
January 24, 2025

Last year, LWN examined the changes lined up for Rust's 2024 edition. Now, with the edition ready to be stabilized in February, it's time to look back at the edition process and see what was successfully adopted, which new changes were added, and what still remains to work on. A surprising amount of new work was proposed, implemented, and stabilized during the year.

Editions are Rust's mechanism for ensuring stability in a language that makes frequent, small releases, and which is still evolving quickly. Each edition represents a backward-compatibility guarantee: once the edition is finalized, code that compiles on that edition will always compile on that edition. The editions aren't totally frozen — the language can still add new features, so long as they're backward compatible — but the project takes the commitment to backward compatibility seriously. New releases of the compiler are tested against most of the published Rust code on crates.io and the Rust-for-Linux kernel code to ensure that they don't break code written for old editions.

Therefore, the introduction of a new edition represents the language's only chance to introduce backward-incompatible changes. Over the past year, a good number of such changes have piled up, from minor changes to the syntax, all of the way through changes to how certain types are interpreted by the type system. While none of the changes are essential for Rust projects to immediately adopt, they do represent something to be aware of when writing Rust code.

Scope changes

One problem that has vexed Rust programmers for some time is the way that the language handles the lifetime of temporary values. For example, the if let construction lets the programmer fallibly unwrap a value. In the current version of Rust, however, it has the annoying property of keeping any temporaries involved in the match alive until after the corresponding else block. For example, in this code the std::sync::Mutex remains locked in the else branch:

    if let Some(x) = mutex.lock().some_method() {
        ...
    } else {
        // Mutex remains locked here
    }

Changing this is a backward-incompatible change, because Rust programs can be sensitive to the order in which items are dropped. In particular, any program that relied on the mutex remaining locked in the else block would be broken if the language changed to drop temporary items earlier. That change is ready to be enabled in the 2024 edition, along with a linting rule to warn programmers about affected programs.

The project is also fixing the order in which temporary expressions at the end of a block get cleaned up. Rust blocks have return values, which lets if statements be used as expressions, among other cases. But when an expression is returned from a block, it might create temporary values. For example, this code creates a temporary value for cell.borrow():

    fn f() -> usize {
        let cell = RefCell::new("..");
        // The borrow checker throws an error for the below
        // line in the 2021 edition, but not the 2024 edition
        cell.borrow().len()
    }

Currently, that temporary value is dropped after the locals from the block (just cell, in this case) are dropped. This is somewhat inconvenient, because it means that the above code is rejected by the lifetime checker, since a use of cell lives longer than cell does. Programmers have to work around it by assigning any temporaries to variables explicitly, cluttering the code with unnecessary let statements. So the 2024 edition will fix that problem by ensuring that temporaries from the end of a block are dropped before local variables.

Unsafety

A new warning will be issued for unsafe operations in unsafe functions. In current editions of Rust, marking a function as unsafe actually conflates the two meanings of the unsafe keyword: it both marks the function as unsafe to call, and allows the programmer to use unsafe operations in the body of the function. While this makes intuitive sense, Rust programmers have generally found that it invites confusion, because it can make it hard to spot which operations in the body of the function actually are unsafe. In the new edition, using an unsafe operation in an unsafe function without an additional unsafe block to mark the usage will result in a warning.

The biggest change to the rules for unsafe in this edition, however, is to how the language handles extern blocks (which declare external functions that can be called using the C foreign-function interface). Currently, all external functions are treated as unsafe, because the C code could potentially do anything. This is somewhat un-ergonomic for simple functions that, in fact, are safe to call. But just letting the programmer assert that some external functions are safe would break Rust's promise that, when undefined behavior occurs, there must be one or more incorrect unsafe blocks that are responsible.

The solution that the language developers have landed on is unsafe extern blocks. Now, the entire extern declaration will be marked unsafe — and the user is thereby duly warned that they must ensure that all of the external function declarations (including the safety of each individual function) are correct, or else face the risk of undefined behavior. Since that warning has moved to the point of declaration, uses of foreign functions no longer necessarily require the use of an unsafe block (although the programmer can still mark a foreign function as unsafe explicitly). In practice, since many Rust projects use auto-generated C bindings, the difference is likely to be minor. The change will reduce the number of unnecessary unsafe blocks used for calling external functions, even if it also adds a step of indirection to chasing down any problematic unsafe code.

The edition will also make the std::env::set_var() and std::env::remove_var() environment-variable manipulation functions unsafe, since they are only supposed to be used in single-threaded environments, but nothing in their types enforces that. Similarly, the edition will require the no_mangle attribute (which disables name mangling) to be marked unsafe, since it can cause the wrong function implementation to be called on some platforms if there are two functions with the same name. It will also no longer be possible to take references to global mutable variables — those must be manipulated with raw pointers in unsafe blocks, instead.

Capturing lifetime information

LWN has covered the work to improve the usability of inferred function return types, although the work has not stopped since that article was published. To briefly recap: the Rust compiler allows programmers to say that a function returns impl Trait for some trait, and the compiler will automatically infer which specific concrete type the function should return. This is a kind of existential type that is particularly useful for functions that return complicated iterators, which have complex types that it would be redundant to have the user write out. The Rust developers call the feature "return-position impl Trait" (RPIT) types.

The problem comes when this feature has to interact with Rust's lifetime system. If the type that the compiler infers contains a reference with a given lifetime, previous versions of Rust had no way for the programmer to name that lifetime, and therefore no way for the programmer to add any bounds to it. This proved to be particularly problematic for asynchronous functions that frequently need to hold references with complex lifetimes. To fix this, the language added a new piece of syntax that allows the programmer to name the lifetimes used in an impl Trait type. For example, this function explicitly says that the lifetime 'a is referenced by the (implicit) return type, allowing the programmer to work with the lifetime by name if needed:

    fn example<'a>(one: &'a Foo, two: &Bar) -> impl use<'a> Sized {
        ...
    }

Any lifetimes not mentioned in the use block are not captured, so in the above example two is only used inside the function, not stored in its return type. This solves one of the main problems with RPIT types, and is a good step toward improving the ergonomics of asynchronous functions, which was one of the Rust project's goals for 2024. But the semantics of impl Trait types are currently a bit inconsistent. When there is a use block, both lifetimes and generic type parameters that are captured by the return type must be listed explicitly. When there is no use block, generic type parameters are captured implicitly, but lifetime parameters are not (since that would have avoided the whole problem by giving them names).

In order to make impl Trait types more consistent, the 2024 edition of the language will use the same rules for both lifetime parameters and generic type parameters: they will be implicitly captured by default, and if the programmer wants to give them names (or only capture a subset of them), then they must use a use block.

Small changes

Cargo, Rust's package manager and build system, is also seeing some changes. The largest one is that it will soon support selecting dependencies with compatible minimum-supported Rust versions. So, for example, a library that declares that it supports Rust version 1.76 will no longer be able to silently depend on libraries that only declare support for Rust version 1.80. This will solve a number of headaches for maintainers.

There are many more small changes to the language that are less likely to require changes to existing code:

Although there are a great number of backward-incompatible changes destined to land in this edition, adapting to the edition should not be too difficult. One of the requirements for any change proposed for an edition is that there be a way to automatically migrate existing code. Any Rust project using Cargo should be able to run cargo fix --edition and then update the edition declaration in its Cargo.toml file in order to upgrade. Some of the automatic fixes are a little bit ugly (or introduce new unsafe blocks), and so the process is not completely painless. Since Rust will happily mix libraries with different editions, however, many projects will choose to stay on the 2021 edition for some time.

The Rust project did not quite accomplish all of the goals it set for 2024 (perhaps explaining why the 2024 edition will be released in February 2025), but it made substantial progress. And, although many people worry about the incremental accumulation of complexity in the language, the plans for the 2024 edition show that the project is willing to simplify and streamline things over time — albeit not as quickly as some people would like. We'll have to see what gets proposed for the 2027 edition over the next few years.



to post comments

Goals update

Posted Jan 24, 2025 20:31 UTC (Fri) by randomguy3 (subscriber, #71063) [Link] (2 responses)

The December goals update came out yesterday (presumably after the article was originally written), and is probably a better summary of what was accomplished in 2024 than the November update linked in the article.

Goals update

Posted Jan 24, 2025 20:35 UTC (Fri) by daroc (editor, #160859) [Link]

Yeah, I saw the December update in my RSS feed just after sending this out. That's timing for you.

Goals update

Posted Jan 24, 2025 20:37 UTC (Fri) by randomguy3 (subscriber, #71063) [Link]

It's possibly also worth noting that the goals were actually for 2024H2, despite the title of the original blog post being "Rust project goals for 2024". It seems the Rust project is intending to work on a six-month roadmap, with the next set of goals being for 2025H1.

match ergonomics != macros-by-example

Posted Jan 29, 2025 20:27 UTC (Wed) by snorehart (subscriber, #139231) [Link] (1 responses)

I believe the linked bug at "tweaked" (https://github.com/rust-lang/rust/issues/131414) is not about declarative macros as implied, but actually about match ergonomics (elision of references and dereferencing in `match` expressions, not specific to macros).

match ergonomics != macros-by-example

Posted Jan 29, 2025 20:38 UTC (Wed) by daroc (editor, #160859) [Link]

Yes, I think you're right; I must have captured the wrong link while going down my list. Thanks for pointing this out.

Forward progress on never type

Posted Feb 5, 2025 16:35 UTC (Wed) by apoelstra (subscriber, #75205) [Link]

Really happy to see these changes to type inference to support `!`. Glad that the Rust developers were willing to cause a little bit of ergonomic pain when it comes to generic types alongside diverging paths. I think the new behavior better-reflects what is going on in the compiler and illustrates exactly where the old behavior could let unsoundness seep in.


Copyright © 2025, Eklektix, Inc.
This article may be redistributed under the terms of the Creative Commons CC BY-SA 4.0 license
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds