Status quo: Grace waits for gdb next
🚧 Warning: Draft status 🚧
This is a draft "status quo" story submitted as part of the brainstorming period. It is derived from real-life experiences of actual Rust users and is meant to reflect some of the challenges that Async Rust programmers face today.
If you would like to expand on this story, or adjust the answers to the FAQ, feel free to open a PR making edits (but keep in mind that, as they reflect peoples' experiences, status quo stories cannot be wrong, only inaccurate). Alternatively, you may wish to add your own status quo story!
The story
Grace wants to walk through the behavior of a toy program.
She first fires up cargo run --verbose
to remind herself what the path to the target binary is. Part of the resulting Cargo output is:
Running `target/debug/toy`
From that, Grace tries running gdb
on the printed path.
gdb target/debug/toy
and then
(gdb) start
to start the program and set a breakpoint on the main
function.
Grace hits Ctrl-x a and gets a TUI mode view that includes this:
│ 52 } │
│ 53 │
│ 54 #[tokio::main] │
│B+>55 pub(crate) async fn main() -> Result<(), Box<dyn Error + Send + Sync + 'static>> { │
│ 56 println!("Hello, world!"); │
│ 57 let record = Box::new(Mutex::new(Record::new())); │
│ 58 let record = &*Box::leak(record); │
│ 59
Excitedly Grace types next
to continue to the next line of the function.
And waits. And the program does not stop anywhere.
...
Eventually Grace remembers that #[tokio::main]
injects a different main function that isn't the one that she wrote as an async fn
, and so the next
operation in gdb
isn't going to set a breakpoint within Grace's async fn main
.
So Grace restarts the debugger, and then asks for a breakpoint on the first line of her function:
(gdb) start
(gdb) break 56
(gdb) continue
And now it stops on the line that she expected:
│ 53 │
│ 54 #[tokio::main] │
│ 55 pub(crate) async fn main() -> Result<(), Box<dyn Error + Send + Sync + 'static>> { │
│B+>56 println!("Hello, world!"); │
│ 57 let record = Box::new(Mutex::new(Record::new())); │
│ 58 let record = &*Box::leak(record); │
│ 59 │
│ 60 let (tx, mut rx) = channel(100); │
Grace is now able to use next
to walk through the main function. She does notice that the calls to tokio::spawn
are skipped over by next
, but that's not as much of a surprise to her, since those are indeed function calls that are taking async blocks. She sets breakpoints on the first line of each async block so that the debugger will stop when control reaches them as she steps through the code.
🤔 Frequently Asked Questions
Here are some standard FAQ to get you started. Feel free to add more!
What are the morals of the story?
- A common usage pattern: hitting
next
to go to what seems like the next statement, breaks down due to implementation details of#[tokio::main]
andasync fn
. - This is one example of where
next
breaks, in terms of what a user is likely to want. The other common scenario where the behavior ofnext
is non-ideal is higher-order functions, likeoption.and_then(|t| { ... }
, where someone stepping through the code probably wantsnext
to set a temporary breakpoint in the...
of the closure.
What are the sources for this story?
Personal experience. I haven't acquired the muscle memory to stop using next
, even though it breaks down in such cases.
Why did you choose Grace to tell this story?
I needed someone who, like me, would actually be tempted to use gdb
even when println debugging is so popular.
How would this story have played out differently for the other characters?
* Alan might have used whatever debugger is offered by his IDE, which might have the same problem (via a toolbar button that has the same semantics as `next`); but many people using IDE's to debugger just naturally set breakpoints by hand on the lines in their IDE editor, and thus will not run into this.
* Most characters would probably have abandoned using gdb much sooner. E.g. Grace may have started out by adding `println` or `tracing` instrumentation to the code, rather than trying to open it up in a debugger.