Rust Regression: Format_args! And Temporary Lifetimes
Hey guys! Let's dive into a fascinating regression issue that popped up in Rust, specifically between versions 1.88.0 and 1.89.0. This issue revolves around the format_args!
macro and how it handles temporary lifetimes. It's a bit of a rabbit hole, but stick with me, and we'll get through it!
The Problem: Temporary Value Dropped While Borrowed
So, what's the gist of this regression? In Rust 1.89.0, some code that compiled perfectly fine in 1.88.0 started throwing a E0716
error: temporary value dropped while borrowed. This error essentially means that a temporary value, which is being borrowed, is being dropped before the borrow is finished. Ouch! That's a big no-no in Rust's world of ownership and borrowing.
Diving Deep into the Code
To really understand this, let's look at the code snippet that triggered the issue:
#[derive(Debug)]
struct Thing;
#[derive(Debug)]
struct Ref<'a>(&'a Thing);
impl Drop for Ref<'_> {
fn drop(&mut self) {}
}
fn new_thing() -> Thing {
Thing
}
fn new_ref(x: &Thing) -> Ref<'_> {
Ref(x)
}
pub fn foo() {
let _x = format_args!("{:?}, {:?}", 1, new_ref(&new_thing()));
}
In this code, we've got a Thing
struct and a Ref
struct that holds a reference to a Thing
. The Ref
struct also implements the Drop
trait. The foo
function is where the magic (or rather, the bug) happens. It uses format_args!
to create a formatted string, which includes a new_ref
created with a temporary Thing
. It's this interaction with the format_args!
macro and the temporary Thing
that's causing the trouble.
Breaking Down the Error
The compiler error points to this line:
let _x = format_args!("{:?}, {:?}", 1, new_ref(&new_thing()));
The error message tells us that the temporary value created by new_thing()
is being dropped at the end of the statement, while it's still being borrowed by the Ref
struct. This is a classic lifetime issue. The borrow checker is doing its job, preventing us from creating a dangling reference.
Why the Regression?
So, why did this code compile in Rust 1.88.0 but not in 1.89.0? The answer lies in changes to how format_args!
handles temporary lifetimes. In 1.88.0, the macro seemed to be extending the lifetime of the temporary, perhaps unintentionally. However, in 1.89.0, the macro's behavior was tweaked, and the temporary's lifetime was no longer extended, exposing the lifetime issue.
The format_args!
Macro: A Deep Dive
To truly grok this issue, let's demystify the format_args!
macro. What exactly does it do, and why is it so central to this lifetime puzzle?
The format_args!
macro in Rust is a powerful tool for creating formatted strings without immediately allocating memory. Think of it as a recipe for a string, rather than the string itself. It takes a format string and a list of arguments, and it constructs a std::fmt::Arguments
struct. This struct holds all the information needed to format the string, but it doesn't actually do the formatting until later.
The real magic of format_args!
lies in its ability to be zero-cost. It avoids allocating a string buffer upfront. This makes it incredibly efficient for logging, debugging, and other scenarios where you need to format strings frequently but might not always need the final result. The actual formatting is deferred until the Arguments
struct is used, often by functions like println!
or format!
. This deferred formatting is what allows format_args!
to be so performant.
How format_args!
Works Under the Hood
Under the hood, format_args!
constructs an Arguments
struct that contains:
- A reference to the format string.
- A list of arguments, which can be values or references to values.
- Information about how to format each argument (e.g., debug formatting, display formatting, etc.).
The key here is that format_args!
often works with references. It doesn't always own the data it's formatting; it borrows it. This is where lifetimes come into play. The Arguments
struct needs to ensure that any borrowed data outlives the struct itself. If not, you'll end up with dangling references, which Rust fiercely protects against.
The Lifetime Challenge with Temporaries
Now, let's bring this back to our original problem. When you pass a temporary value (like the result of new_thing()
) to format_args!
, the macro needs to borrow that value. But temporaries, by their nature, have a short lifespan. They're typically dropped at the end of the statement where they're created.
This creates a lifetime mismatch. The Arguments
struct created by format_args!
might need to live longer than the temporary value it's borrowing. If the temporary is dropped too soon, the Arguments
struct will hold a dangling reference, leading to a crash or undefined behavior.
This is precisely what the compiler is complaining about with the E0716
error. It's saying, "Hey, you're trying to borrow a temporary value, but that value might not live long enough!"
Why Deferring Formatting Matters
The deferred formatting of format_args!
is what makes this lifetime issue tricky. If format_args!
immediately formatted the string, the temporary value would be used and dropped within the same statement, and there wouldn't be a lifetime problem. But because the formatting is deferred, the borrow of the temporary value needs to last longer, potentially outliving the temporary itself.
In essence, format_args!
is a powerful tool, but it requires careful attention to lifetimes, especially when dealing with temporary values. Understanding how it works under the hood helps to demystify errors like E0716
and write more robust Rust code.
Why the Drop
Trait Makes It Interesting
The presence of the Drop
trait in the Ref
struct adds another layer of complexity. The Drop
trait's drop
function is called when a value goes out of scope. In our case, the Ref
struct's drop
function is empty, but the fact that it exists is crucial.
When a value with a Drop
implementation is dropped, Rust needs to ensure that any borrows held by that value are still valid during the drop process. This is because the drop
function might access those borrowed values. In our case, the Ref
struct holds a reference to the temporary Thing
. If the Thing
is dropped before the Ref
, then the Ref
's drop
function would be accessing a dangling reference, which is unsafe.
The borrow checker is particularly cautious when dealing with types that implement Drop
. It knows that the drop
function is a potential source of unsafety, so it enforces stricter lifetime rules. This is why the E0716
error specifically mentions the Drop
code for the Ref
type.