How do you guys do debugging in Rust?

Yes I mean watchpoints on variables or values of custom Debug implementations. For complex types I usually implement a custom Debug trait. This means that when the Debug::fmt() function is called it will print values that are not necessarily part of the struct or enum that the Debug is implemented on. For example:

struct Data {
    vec: Vec<T>
}

impl std::fmt::Debug for Data {
    fn fmt(&self, f: &mut fmt::Formatter<`_> -> fmt::Result {
        write!(f, "{}", format!("Data {{ vec: {:?} }}\nlength: {}", &self.vec, self.vec.len())
    }
}

Which could yield:

Data { vec: [1, 2, 3, 4] }
length: 4

Would it be possible to make a debugger that could watch for when length >= 5 for example? This is very custom, but it could make the debugger very powerful.

Do you have a sponsor page on github? Are you still actively developing the debugger?

Interesting. I assume that they are basing their debugger on lldb in the same way as in vs code?

Can you make conditional breakpoints on variables with the Rust Rover debugger?

Yep, currently with BS you can do this (GitHub - godzie44/BugStalker: Rust debugger for Linux x86-64):

watch (~vec).len

This will set a watchpoint on the vector length and your program will stop when it changes. (~vec means using the original vector representation with capacity and length). Unfortunately, conditions are not supported now.

Yes, I'm developing the debugger in the "develop" branch. I think the new feature release will be available within 3-4 months. If you are interested in conditional breaks (and watchpoints), just create an issue. I think I will be able to implement this within a reasonable time frame.

2 Likes

I guess the question was "is it possible to set a watchpoint on something exposed only through Debug implementation, ideally without breaking on each change in Debug output, but only on the relevant part?", since in the example vec is not a directly observable variable, but a private field inside one. Or privacy don't matter here?

I can't speak to this specific one, but yes, traditionally at least debuggers ignore visibility, debuggers normally need to reimplement expression evaluation anyway and they already have access to private names for being able to dump the internal structure of types via debug info.

It doesn't matter, so you can do somethink like:

watch (~my_struct.vec).len
1 Like

The problem with watching on Debug output would be that it would be extraordinarily inefficient. You would basically have to break after every instruction and evaluate if the watch is triggered.

Normal watch points on a memory address are hardware assisted. The CPU has hardware watch point support (generally just allowing a few addresses to be watched at the same time, I haven't checked in recent years, but 1-4 addresses were typical some years ago).

4 Likes

This might be a stupid suggestion, but have you tried making extensive use of the newtype pattern and encoding your invariants on them, be it through the type system if possible or else through lots of debug assertions? They will be ckecked in debug buils but completely removed in release, thus adding no cost.

I've never written anything as complex as what you are describing, but I've found that newtypes help me a lot for ensuring correctness.

1 Like

That sounds great!

I have used your debugger and I am very impressed. It crashed sometimes and inspecting the receiver self is not always possible for me?

I mostly use a debugger to understand control flows and complex algorithms. I think that many of the features in lldb are sort of anti-features due to Rust's borrow checker.

Maybe my workflow is not completely optimized yet but I really believe that Rust should have a debugger ideally an official one (BugStalker as an initial starting point maybe?). The debugger should primarily be for inspection, understanding, and speed up on debugging complex control flows found in algorithms. What do you guys think?

No I do not think so!

Can you show an example?

I understand it as:

struct NewType(Vec<ComplexType>);

impl NewType {
    fn asset_invariant(&self) -> anyhow::Result {
        ensure!(<assert_function>);
    }
}

Is that correct?

Yes. If you are ok with your invariants being checked at runtime even in production, this is a good way to do it.

If you only want the checks to be executed in non-optimised (non-prod) builds, you can use debug_assert! instead.

There are lots of great crates to help with creating newtypes. For example nutype which helps a lot to reduce the boilerplate around newtypes if you want them to implement the usual traits. There are other crates that provide commonly needed refinement types, like non-empty collections, so you don't have to reinvent the wheel.

I find that using precise types makes my code more expressive and less error-prone. If you have a function that expects an email address, the argument shouldn't be &str, it should be an email address, because most strings are not valid email addresses. It also helps to disambiguate function arguments, if there are multiple with the same type. I also like to define enums instead of using booleans, even if there will only ever be two states. It's just so much clearer to read onboard_customer(Governmental::Private, KeyAccount::Yes) than onboard_customer(false, true).

Could you describe the cause of the crash in more detail? Ideally, it would be great in the form of an issue on GitHub :slight_smile:

It's true, so in BS i just abbadoned watchpoints that executing program instruction by instruction and limit watchpoints with maximum 4 (using x86-64 hardware breakpoints)

I would usually inspect the logs first, and if I can't figure out what has gone wrong, I'd start debugging using breakpoints in VSCode.

Yes, they're based on a custom modification of LLDB, like VS Code (but it's not the same).

EDIT: Disregard what I initially wrote below. Breakpoints, conditional or not, work fine with Jetbrains tools, provided you're using the GNU toolchain instead of LLDB, at least in Windows. I haven't checked that in Linux for a long time.

You normally can do conditional breakpoint on IntelliJ platforms, including IntelliJ IDEA with the Rust plugin or RustRover, but it's not working at the moment; changing a breakpoint into a conditional breakpoint launches all the tests (and ignores most of them) instead of the intended one, and of course, doesn't stop at the breakpoint.

The best option is to write the condition in the code and place a breakpoint on a dummy statement:

if test_id == 2 {
    let x = 0; // <== put a breakpoint here
}

The debugger for the Rust language is currently in pretty bad shape. Unfortunately, it doesn't seem to improve with time (I suspect their priority is on new features instead of debugging their impressively large number of issues).

For one, if you manage to get the breakpoint to stop as expected, it's almost impossible to inspect the variables: most of the time, it'll say the variable is unavailable, and when it is, you can only see the most basic types like integers and booleans.

At least in Windows; maybe it works a little better in Linux because the version of LLDB isn't 5-year old.

I still like the IDE, but I'm doing all the debugging by printing the values I need to inspect. It's the only way for now.

In general gdb will work better than lldb. See also Debugging support in the Rust compiler - Rust Compiler Development Guide

If you mean debugging rustc, no a debugger isn't necessary most of the time. We have a lot of debugging flags in rustc which print internal representations of the compiler state at various stages in formats that are much more readable than a debugger would print. For example -Zunpretty=hir for HIR and --emit mir for MIR. As well as logs for many important things you can see using RUSTC_LOG=crate_to_print_logs_for. As such you don't need to recompile rustc with full debuginfo and attach a debugger very often to debug issues in rustc. In any case we are actively working on fixing foundational issues with rustc like the new trait solver which fixes a lot of soundness holes in the old trait solver. And a lot of PRs to rustc are bugfixes and general cleanups, not new features.

1 Like

GDB cannot be used in the Windows environment, at least not with the MSVC native toolchain, unfortunately.

EDIT: Scratch that. Now, it's perfectly possible to use RustRover with a stable GNU native environment both for compiling and debugging:

rustup toolchain install stable-x86_64-pc-windows-gnu
rustup default stable-x86_64-pc-windows-gnu

And the conditional breakpoints work with that toolchain! :smiley:

I'm not sure what you mean. It is possible to work without a debugger, but it's very tedious because the user has to add a lot of print statements (that must be removed later) instead of placing conditional breakpoints and inspecting variables. In other languages like Kotlin, it's even possible to inspect expressions with IntelliJ. None of that is possible with the Rust plugin or RustRover (which is the same).

I don't know why you're mentioning the compiler stages or rustc, which is the Rust compiler.

That's indeed my point. There are a lot of bugs, and the number is increasing. Youtrack has changed, so I don't find how to see only the open issues, but I have more than 130 issues reported and less than half of them have been closed (fixed, abandonned, changed into other bugs, or duplicated). Many of the open issues are several years old.

What I'm saying is that for rustc specifically the debug output is generally more useful than what a debugger would print even in an ideal world. If I want to take a look at the MIR, --emit mir will print it in a nicely readable format, while a debugger would show all kinds of internal details that aren't relevant 99% if the time. And the debug outputs work even with a production version of rustc enabling you to investigate a bug within seconds, while a debugger requires you to rebuild rustc with full debuginfo, which can easily take 10min+ (or even longer if you need to investigate an issue with an older rustc version for which the precompiled LLVM is no longer available)

I don't think we are getting buggier. It is just that more people use rustc and thus find more edge cases.

You mean https://youtrack.jetbrains.com/projects/RUST/issues for RustRover issues? That has nothing to do with rustc or the rust project. That is solely the responsibility of IntelliJ. RustRover doesn't have any code shared with rustc unlike rust-analyzer. (with the exception of the proc macro server that rust-analyzer also uses, but using the same proc macro server is unavoidable if you want to support proc macros on stable rustc)

3 Likes

You're the one quoting me and bringing up rustc; I was answering a post about RustRover, which is a JetBrains product (IntelliJ is another of their product, not the company name). :slight_smile:

I think it depends on what one is trying to debug and also on one's preferences.

I usually find it more straightforward to place a breakpoint and follow a few steps to understand why something isn't working, interactively inspecting values. The debugger lets you watch only the variables you care about, or by default, shows the few local variables, so there's not "all kinds of internal details". I can have a quick glance at the few relevant variables instead of looking at a long log that I have to filter out. For instance, I'm working on a compiler, and if I had to output a log of all the variables at each step until I find what happened for a specific input token, I'd spend hours. Instead, I can place a conditional breakpoint on the token value, and step through a few lines. It's a very surgical and efficient operation.

Well, it ideally would, if the debugger was working correctly. :wink: As a "temporary" work-around, I print the relevant values. At least, it's well formatted. EDIT: As I wrote in my edited posts above, the GNU toolchain seems to behave better. Hurrah.

My bad. I thought this was about Rust as a whole rather than specifically Rust in the IntelliJ platform.

1 Like