Extra Meal Rust 2021 Edition Released

Rust 2021 Edition is Released! #

Hello, I am Chen Tian.

The long-awaited Rust 2021 edition (hereafter referred to as the “edition”) has finally been released along with version 1.56. After updating the toolchain with rustup update stable, everyone can try upgrading their previous code to the 2021 edition.

The process is very simple:

  1. cargo fix --edition
  2. Modify Cargo.toml, replacing edition = “2021”
  3. cargo build/cargo test to ensure everything is normal

Before doing the first step, remember to commit any uncommitted code.

If you are new to Rust, you may not be clear about the role of “editions” in Rust, which is a very clever and backward-compatible release tool.

I don’t know if there’s a similar concept in other programming languages, but in the languages I know, there’s nothing like it. C++ allows you to compile lib A with –std=C++17 and compile lib B with –std=C++20, but this usage has many limitations and is not as clean as editions. Let’s first come to a common understanding of it before discussing the key content of this “edition” update.

In Rust, there may be different reserved keywords and default behaviors between editions. For example, the async/await/dyn of 2018 were strictly reserved as keywords, while in 2015 they were not.

Suppose that during the iterative process, the language discovers the need to reserve the keyword actor, but setting it as a reserved keyword would break compatibility and prevent previous code using actor as a common name from compiling. What to do? Upgrade to a major version and split the code into incompatible v1 and v2? This issue gives all language developers a headache.

Languages are always evolving, but it is not uncommon to have to make disruptive updates later due to unfavourable considerations at the beginning.

The previous common way of dealing with this type of problem was to upgrade the major version number.

However, for library authors, if they do not want to upgrade to a major version or are limited by certain reasons and cannot upgrade quickly, in the end, either the developers using this library have to stick with v1, or the developers using this library have to find a compatible alternative for v2. Either way, the entire ecosystem will be torn apart.

Rust cleverly solved this issue by using “editions”. The library author still publishes his code with the old edition, and developers using the library can choose the latest edition they want to use. During compilation, the Rust compiler compiles old libraries with the old edition’s features, while compiling the user’s code with the new edition.

Let’s look at a practical example. I randomly searched for a library rbpf on crates.io, which last update stopped three years ago. Looking at its Cargo.toml, it is a 2015 edition library (not declaring an edition implies 2015), and it is two generations behind current code. Let us try to create a 2021 edition crate, bring in this library, as well as the 2018 edition futures library, and see if there are any issues.

Firstly, make sure your Rust is upgraded to 1.56. Then cargo new test-rust-edition. In the generated project, modify Cargo.toml to add:

[package]
name = "test-rust-edition"
version = "0.1.0"
edition = "2021"

[dependencies]
rbpf = "0.1.0"
futures = "0.3"

I intentionally put together two inherently incompatible crates to see if they can work together. Futures uses async/await, which are keywords introduced in Rust 2018, but rbpf uses the 2015 edition.

After modifying Cargo.toml, we copy into src/main.rs:

use futures::executor::block_on;

fn main() {
    // This is the eBPF program, in the form of bytecode instructions.
    let prog = &[
        0xb4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov32 r0, 0
        0xb4, 0x01, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, // mov32 r1, 2
        0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, // add32 r0, 1
        0x0c, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // add32 r0, r1
        0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // exit
    ];

    // Instantiate a struct EbpfVmNoData. This is an eBPF VM for programs that
    // takes no packet data in argument.
    // The eBPF program is passed to the constructor.
    let vm = rbpf::EbpfVmNoData::new(Some(prog)).unwrap();

    block_on(async move {
        dummy(vm.execute_program().unwrap()).await;
    });
}

async fn dummy(result: u64) {
    println!("hello world! Result is {} (should be 0x3)", result);
}

It doesn’t matter what this code is doing, we just care whether it can run in the 2021 edition crate. After running cargo run, we discover rbpf and futures coexist harmoniously.

A single piece of code uses three editions’ codes yet connects seamlessly; when we use it, we don’t even need to care which edition it is. Impressive, isn’t it?

Thus, editions act as a firewall, preventing the fragmentation of the entire ecosystem. Without changes, everyone can still perform their functions. This is the greatest contribution of editions to Rust. If you’ve experienced the immense pain of upgrading from Python 2 to Python 3, you’d appreciate the significance of this critical concept introduced by Rust.

What’s New in Rust 2021? #

After you understand the significance of the Rust 2021 edition, let’s take a look at some of the most impactful updates.

Disjoint Capture in Closures #

Before 2021, even if you only used one field, closures would need to capture the entire data structure, even if it was a reference. But after 2021, closures can capture only the fields they need.

For example, consider the following code:

struct Employee {
    name: String,
    title: String,
}

fn main() {
    let tom = Employee {
        name: "Tom".into(),
        title: "Engineer".into(),
    };

    drop(tom.name);

    println!("title: {}", tom.title);

    // This line wouldn't work before but can compile in 2021
    let c = || println!("{}", tom.title);
    c();
}

The benefit of disjoint capture in closures is that those closures that captured part of a struct’s fields while other parts of the struct are used elsewhere – which would not compile in 2018 – can now compile. Previously, you had to clone() the struct to satisfy both, but now it compiles.

Feature Resolver #

Dependency management is a challenge, and one of the most difficult parts is selecting the version of a dependency to use when relying on multiple different packages. This refers not only to their version numbers but also to the features that are enabled or not for that package. Because Cargo by default merges features used in multiple references to a single package.

For example, suppose you have a crate named Foo with features A and B, which are used by packages bar and baz, but bar depends on Foo + A, while baz depends on Foo + B. Cargo will merge these two features and compile Foo + A B. - Image

It has a benefit that you only need to compile Foo once, and it can be used by bar and baz. But what if A and B were not meant to be compiled together? If you’re interested in this scenario, you can check out the link to Rust 1.51 compilation strategy below. This has been a long-standing problem in Rust and has troubled the community.

Previously, Rust 1.51 finally provided a new way of dealing with this issue through different compilation strategies. Now, this strategy has become the default behavior for 2021. It does bring some compilation speed loss, but it makes the result more accurate.

New Prelude #

Every language will import some very common behaviors from certain namespaces by default to make it convenient for developers. Rust is no exception. It imports some traits, data structures, and macros by default, such as the From/Into traits we use, structures like Vec, and macros like println!/vec!. This way, you don’t need to frequently use use.

In the 2021 edition, TryInto, TryFrom, and FromIterator are included in the prelude by default, so we no longer need to declare them with use. For example, the following statement is now unnecessary since the prelude already contains it:

use std::convert::TryFrom;

Summary #

Overall, Rust 2021 is not a major edition update; it contains only a few changes that are incompatible with previous versions. Rust will remain stable on this edition for the next three years.

You might wonder: all this fanfare for just this? But this is the heart of Rust’s thoughtful design.

Within every three years, iterating new features uninterruptedly every six weeks, but without introducing breaking changes or isolating them with some compile options that must be manually turned on (such as resolver = “2”); upon completion of the three-year period, upgrade the edition, introducing all the potential breaking changes from the past three years and foreseeable future breaking changes (such as reserving new keywords) all at once through editions.

The fewer major actions in an edition, the more mature the language is heading toward.

Alright, that’s all for the introduction of the 2021 edition. There are some other changes that I won’t elaborate on here. If you’re interested, you can look at the release documentation. The code repository for this course, tyrchen/geektime-rust, has also been upgraded to the 2021 edition, and you can see the specific modifications in this pull request.