Have you been contemplating a switch to the Rust 2024 edition for your project, but are wondering how to go about it?

This is a description of my experience upgrading to the 2024 edition in a very large workspace. The workspace has close to 400 crates, and more than 1500 rust dependencies. It has a few years of history, and has been touched by a lot of developers with varying experience levels. Everything works on rust 1.84 using the 2021 edition, with no clippy warnings using the default lint settings.

The usual advice for upgrading to the new edition goes like this:

  • Run cargo fix --edition
  • Add edition = "2024" to Cargo.toml

That's not how I'm going to approach the upgrade. My reasons are:

  • It's too big a change to take on all at once. This is a production codebase, and we need to be able to fix regressions. One giant commit touching thousands of lines of code across hundreds of crates doesn't lend itself to easy debugging or bisecting.
  • We don't want all of the automatically generated changes. In a lot of places we would prefer the shorter temporary lifetimes in if-let expressions, rather than transforming them to match expressions. And most of our macros will work just fine with the newly-expanded expr definitions.
  • I want to be able to explain the changes made to other engineers. To do that, I need to understand them myself.
  • I want to be able to make changes in stages. If I can detect and fix issues ahead of time, in a way that's compatible with both the 2021 and 2024 editions, I definitely want to do it that way. This also means that changes can easily be reverted if I break something.
  • The bigger the change, the longer it's going to take to code review. I want to see meaningful reviews, and that means limiting the size of each change.
  • Any project with a lot of engineers has a lot of concurrent work. It's probably a bad time when you need everyone to stop working while a huge change lands. If we can make this transition look like ordinary daily progress, then that's better for everyone.
  • It's a learning experience for all of us. I learned a bunch of new things about the language by going through this upgrade in detail, and I get to share that knowledge with the team, and add meaningful comments and commit messages conveying our understanding of the code.

A few times I've discovered code that uses tokio File I/O in a way that is unreliable. This is because there's a bit of a footgun in the implementation of how File writes occur in the tokio runtime.

If you use tokio::fs::File in your code, you should probably know about this hazard, to avoid some unpleasant surprises.

This is something I built for my own use, as a reference I can use to remember some of the properties of the Rust channel implementations I use most often.

Which channels implement fallible send? Which one implements an async sender and blocking receiver? What do I lose if I make my channel bounded size? These are the questions I ask myself when picking out an mpsc channel implementation.

I've often wondered about the differences between lazy_static and once_cell, and in Rust 1.70 and 1.80 the standard library is gaining the ability to create one-time-initialized values.

Most big projects end up using one of these crates, because lazy initialization is a very convenient way to implement almost-const global values that can't actually be const-initialized, because they need heap memory (e.g. String or Vec), read files or environment variables, or use types that don't have const initializers yet.

But how do they work? Let's spend some time looking at the differences between these three implementations.

The other day I made a joke on twitter, and learned some interesting things about raw pointers in Rust.

The abridged joke goes something like this:

Yosh: What do you mean Rust doesn't ship with rand built-in?

Me: ASLR to the rescue!

fn main() {
    let rand = main as usize;
    dbg!(rand);
}