As a former C programmer, one of the reasons I like Rust is because it is easier to debug. This is what I would say to the C programmer(s) you mentioned.
Unsafe code in Rust (such as the unwinding code in Rust's core library, which I presume is unsafe) can segfault. That is one of the many, many things that can happen if it triggers undefined behavior (UB). To my knowledge, all of Rust's UB side effects can be triggered by C, even if some are harder to pull off.
Thinking about debugging, the question one must ask is: by the time my program aborted (whether a segfault, hitting an assert, or something else), what else happened before that point? The default behavior of debuggers is to run the program until it traps an abort. That is the state in which you will start the expedition of root cause analysis.
At that point, you are looking at dozens of pottery shards and trying guess what the vase looked like before it shattered. In my experience, this is easier with Rust -- even on unsafe code! -- simply because its memory safety rules mean less goes wrong before the program is stopped.
In C, the heap (if not the stack itself) is often damaged by the time it is stopped. Lots of what Rust would call UB went on before that point, but that's totally fine in C. It wasn't until the program tried to do something completely against the rules -- like accessing memory outside the process address space -- that it was stopped.
By contrast, when I was writing unsafe Rust interacting with C as my first Rust project, Rust's better behavior made memory corruption rarer, narrower, and easier to pin down the vast majority of the time.
If I did not understand an unsafe
contract and triggered UB, what usually happened was that the C code would uncover the issue very quickly, with very few side effects.
An example that happened often at the beginning: suppose something on the heap was implicitly dropped, but I didn't realize that and passed a pointer to it back to C. A classic use after free.
By the time I made that mistake, I had already fought with the borrow checker, which wouldn't let me try to pass the naked reference back. The Box
type made sure I could not pull it out and keep it on the stack. And Rust's own compiler passes made sure that the item was not implicitly dropped so long as I kept using it.
The only thing Rust couldn't check was the validity of the pointer that was returned. It checked the validity when it was created (because it was created from a reference), but the point of a pointer (ahem) is to have an unbounded, a.k.a. "programmer managed" lifetime.
By the time the Rust compiled, that manual lifetime was the only thing wrong. When the program ran, nothing would go wrong in Rust, but the C would segfault on the first access without anything else bad happening.
The first time this happened, it took a while to figure out what went wrong. After all, the debugger was in the C code which had called the Rust when the fault actually occurred. But once I figured it out, the pattern it was easy to recognize.
In the course of that project, it is true that I created some rather nasty situations with UB. My personal "favorite" (which I should do a post about sometime) was getting a single-threaded program to deadlock itself. But every single one could happen in C -- and Rust simply made them much easier to debug than the C would have been.