Rust should own its debugger experience
— 2023-01-12

  1. introduction
  2. integration done right
  3. debuggers today
  4. towards a coherent experience
  5. conclusion

Introduction

40% of Rust Developers believe Rust's debugging experience could use improvement. And that's not surprising: when we write code we make assumptions, and sometimes we assume wrong. This leads to bugs, which we then track down and fix. This is what we call "debugging", and is a core part of programming. The purpose-built tools which help us with debugging are called "debuggers".

Unlike the Rust compiler, the Rust project doesn't actually provide a "Rust debugger". Users of Rust are instead expected to use a third-party debugger such as gdb, lldb, or windbg to debug their programs. And support for Rust in these debuggers is not always great. Basic concepts such as "traits", "closures", and "enums" may have limited support. And debugging async code, or arbitrary user-defined data structures may be really hard if not impossible. This limits the utility of debuggers, and in turn limits the Rust user's debugging experience.

Integration done right

When we interact with the "rust compiler", it's often through either cargo build, or Rust-Analyzer in VSCode 1. The fact that a large chunk of the compiler is in fact LLVM is for all intents and purposes just an implementation detail. Even Rust-Analyzer powering the VS Code features is not something you need to think about most of the time.

This is how it should be. When you install Rust, the right version of LLVM gets bundled in. The Rust project is responsible for LLVM doing the right things for Rust, and we'll sometimes even float patches on a fork to fix things the project depends on. Users should never become aware of these details. We're even at a point where we might replace LLVM with a different backend in certain scenarios 2, and the only thing users should notice are faster build times.

1

VS Code has 75% market share apparently. (src)

2

I'm talking about Cranelift for debug builds here.

Debuggers today

Debuggers have a very different story today. The Rust project doesn't really provide a "debug workflow", meaning Rust users are made largely responsible for figuring that out themselves. This means they need to find a debugger, install it, and figure out how to run it. Whatever Rust support the debugger has is what they get. And hopefully Rust is enough on debugger author's radars that support for it improves over time. It's another question when newer versions of a debugger may become available to Rust users too. As a project we can contribute some patches, but at a fundamental level we don't have any control over these tools. Which means we don't have any control over the resulting user experience.

Work is happening though to enable the output of the Rust compiler to be better understood by third-party debuggers. For example RFC 3191: Debugger Visualizer has been accepted which enables Rust users to author debugger-specific visualizer scripts. This enables library authors to define debugger-specific scripts which can provide more details about types. This is a very pragmatic fix which will enable, say, the stdlib to immediately start providing a better UX for existing debuggers. But we can't reasonably expect all Rust projects to define debugger visualizations for all of their data structures, so as a general solution it has some pretty clear limitations.

Towards a coherent experience

In my opinion the Rust project needs to rethink its relationship to the debugger user experience. Rather than something that we leave up to users to figure out, it should be something that the Rust project is in charge of providing to Rust users. If we want the Rust debugging experience to improve, we in the project need to be the ones responsible for providing that experience.

I'm not suggesting that we start writing debuggers from scratch. Instead I'm suggesting we start packaging and distributing existing debuggers for all platforms, together with plugins which extend those debuggers with Rust-specific functionality. This will enable us to teach those debuggers about things like traits, enums, and even async 3. And we can ensure that the debuggers + plugins are updated regularly, and work correctly. Just like with LLVM, the specific debugger should become just an implementation detail.

3

We can work on ways to extend this to work with ecosystem runtimes as well.

Which debuggers to package 4, and how to distribute them are details I'm confident we can figure out. We've done this before for other tools, so that shouldn't be too different. The main question is how to expose the debugger, and for that I would propose we start with the Debug Adapter Protocol (DAP), LSP's less-famous sibling. Just like we use LSP to provide IDE features in an IDE-agnostic way, we can use DAP to provide debugger support in an IDE-agnostic way.

4

Maybe we don't even want to package certain platform-specific debuggers, and instead we just want to distribute bindings. Maybe different platforms should make different decisions. Or perhaps people should be able to choose. These are the kinds of details I expect experts to want to weigh in on.

I believe by exposing the debugger as a server, we can enable others to write tooling for it. For example: we could write standalone VSCode extensions which integrate with it, or perhaps contribute a DAP integration directly to Rust-Analyzer 5. Perhaps some people prefer an editor-based debugger flow; which should be possible to build against DAP. And should DAP provide limitations, we can always write extensions with the intent to later upstream them - which is again something we do with LSP as well.

5

I defer to the Rust-Analyzer team to comment on what the best course of action here might be.

A really interesting idea in the Rust debugger space has been the approach of plugging a MIR interpreter into a debugger as an extension, which is then able to evaluate Debug impls at runtime. This has been prototyped and shown to be possible. In my opinion that's the ideal solution because it doesn't require any user code to be changed, and it's also exactly the kind of thing that the Rust project can do which external debugger projects can't. Just write your Debug impls, and the debugger will figure out how to interpret them. Making this practical will not be without challenges, but those are mostly technical - and technical talent is something we're not short on in the Rust project.

Conclusion

Those are my thoughts on how I think we can structurally improve the debugging user experience in Rust. I believe that by making the Rust project responsible for the debugger experience we can provide a fundamentally better experience than any external debuggers ever could. And I would be excited if this was something the Debugging WG would seek to pursue.

One last note before publishing: Debugger extensions in VS Code also have support for profiling and even REPL-like evaluation. I think this is part of DAP too, or some extension to it at least. I feel very similar about profiling like I do about debugging: the Rust project should be responsible for providing a premier user experience. But we have to start somewhere, and I think if we can build the integrations needed to support debugging, we can eventually grow the scope to become responsible for profiling too. Go and JS have this already, and I'd love for Rust to catch up.

Thanks to Michael Woerister for reviewing!