What to say to a C programmer when they say …

I have noticed that when discussion turns to Rust there is usually a C/C++ programmer popping up who will try and brush aside all the advantages of Rust's memory safety features. Perhaps we could explore a few examples of that and what is the best way to respond.

A recent example arose when a couple of use were implementing a somewhat complex recursive algorithm using threads in our preferred languages, C, C++ and Rust. Turned out that in all cases the programs crashed and burned with stack overflow when run on the problem size we were aiming at.

This was easily fixed by telling rayon what size stack to use for it's threads. Similarly setting pthreads stack size.

But then an observer commented:

surely Rust allowing a segfault is as bad as C allowing code to stomp all over itself and everything else in memory? (Except that then PC architecture and OS has limited the damage somewhat.)

What is a good response to that?

2 Likes

Rust allowing a segfault means your process will crash. C allowing memory unsafety gets you Heartbleed.

25 Likes

Segfaulting on a deeply recursive algorithm is "just" the program running out of (stack) memory; it is hard to see how Rust would be able to prevent memory exhaustion?

One way to circumvent this is to have the function carry a bump allocator argument, and make your locals come from this allocator.

1 Like

Hi,
I'm not having the perfect reply, and I'm still new to Rust, but as Rust tent to be a functional language it may support tail-call-optimizations like Scala does. This would prevent stack-overflows in function recursion when the function is written to allow tail-call-optimizations on stack usage.

I'm not sure whether this is already possible in Rust but I would bet quite hard to achieve in C/C++ ?

Rust supports no TCO at this time, so that's not a good argument.

This is a better argument, at least for the moment.

Ultimately the best argument is quantitative data showing that in general and across the board, large Rust programs (think Firefox-sized or the size of the Linux kernel, of which there are exactly none in production to my knowledge) have a lot less bugs in them than comparable C/C++ programs, including a lot less security bugs (many of which are currently just memory unsafety issues in disguise).

1 Like

Indeed it is.

On Linux at least there is a difference:

The C version just dies with a segfault which says nothing about what ones problem is:

Segmentation fault (core dumped)

The Rust version produces the following:

thread '<unknown>' has overflowed its stack

thread 'fatal runtime error:
thread '<unknown>
thread '
thread '
...

Presumably if I named my threads they not be < unkown >.

I have changed something... previously it vomited up a thousand jumbled error messages in which a file and line number could be discerned.

This is arguably better than a segfault and no clue. But is it a general feature of Rust or does it happen only on Linux? What if one had such a threaded stack overflow on some RTOS on an embedded system with no MMU for example?

4 Likes

Resource exhaustion is fundamentally different from other memory errors, such as use after free and double free, to say nothing about data races. Rust doesn't prevent resource exhaustion errors, nor does it prevent deadlock. That's not a reason to give up on all its other advantages.

18 Likes

The observer's diagnosis was incorrect. Detected and handled stack exhaustion is not as dangerous as "stomping all over itself and everything else in memory".

Aborting the program before it can cause harm is one of the strategies that both C and Rust use to mitigate errors/vulnerabilities. In this case both handled stack overflow in the same way, but in general Rust has much more compile-time guarantees, so it relies much less on memory errors caught at runtime. Rust has much more precise runtime checks by default too, so it can also guarantee to panic before anything unsafe happens.

There's a significant difference between a detected, controlled and deterministic crash/abort/panic, and a memory corruption that may cause crashes randomly and create vulnerabilities when the attacker can exploit it without or before crashing.

14 Likes

I came to that conclusion this morning. The Rust did actually stop with some error messages rather than the way C just runs into a segfault.

Which prompts me to ask: Is that stack overflow detection and handling part of the Rust language itself? Or is it an accident of being run on an OS/processor with an MMU and capability to raise exceptions when the wrong memory is accessed?

For example: I'm used to C on embedded systems with no operating system. There the C language offers no support for stack overflow detection. The stack pointer will stray into the heap or wherever and your system crashes. Does the Rust language itself ensure better behavior on such systems?

3 Likes

Stack protection in Rust is mostly done by LLVM + OS, but Rust had to generate proper code for it: https://github.com/rust-lang/rust/pull/42816

5 Likes

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.

10 Likes

Thanks for all the great perspectives here.

I'm kind of loath to get into an argument with such a C programmer. They have all kind of ways to wriggle around and defend their position. But there is one concrete thing that gets their attention and they cannot refute, raw speed.

So one of the best arguments I have today is that I can take C/C++ programs, rewrite them as Rust and have them run faster!

Just now I have the following results for one such conversion job. Does not matter what it is here but it's the same algorithm, pretty crudely translated to Rust without much "idiomatization" going on:

pqplum64 (C, pthreads)          1m51s
tatami_rust_threaded64 (rayon)  1m37s
5 Likes

I think it would be handy to document common misconceptions skeptics tend to form WRT Rust. Discussing how to quickly detect the misconceptions mid-conversation (since they're not always plainly laid out) and how to adequately respond to them may help.

I see your situation as a case of black&white thinking: "Rust doesn't fix all problems, so it might as well solve none". In my case, I struggle with people who discard anything that's unfamiliar as 'too complicated' (eg. traits). The issue of how to respond to various detractors sounds like a problem worth solving.

I wouldn't know for sure, but are you being maybe somewhat forceful with them? Maybe explicitly acknowledging their concerns could help them open up and become more receptive.

5 Likes

Just wanted to say, I love how you have to have an impl block for each trait you implement (it's not just all one big mess) and the power you get from implementing a trait only under specific conditions. I didn't even know I wanted it until I used it! :smile:

5 Likes

That is something I have heard and read a few times now.

Quite possibly. I tend to lean towards the Linus Torvalds school of explaining things.

You know how it goes when programmers start discussing programming languages. All of a sudden things get out of hand.

Which is why I posted the question here.

Perhaps it better not to even try. Just get on with Rusting and try and set a good example.

1 Like

Yeah, that's pretty nice!

I know what you mean. You could maybe try attending assertiveness training workshops. Try to be positive though!

As much as we'd like to think that the choice of a particular programming language (or any other tool) is a clean, rational, logical decision -- it's actually a deeply emotional issue. There's no best programming language; there's rarely even a language that's "best" for a particular task and situation. Which language should be used depends on the people doing the using -- and that depends on their emotions.

Because it's an emotional issue, there are two approaches that usually won't work:

  1. Presenting reasoned arguments based on logic.
  2. A negative emotional counterattack.

Even though I knew this already -- I manage programmers -- I was reminded of it when my earlier Rust-over-C essays got very negative responses. I collected the responses and talked them over with my C programmer friends. While some people were trolling or simply being contrary, a lot of the responses came down to fear. I've been spending time since then trying to understand these fears and address them positively instead of negatively (and thus my recent tutorials).

So when someone who speaks C but not Rust points to a Rust behavior and says, "see, doesn't this mean Rust is a house of cards?" the first thing you should do is pause and try and figure out why they are saying this.

  • Maybe they're worried they won't be able to learn another language and the world will move on without them. (Haskell is really good at producing this fear.)
  • Maybe they've seen a dozen "C replacement" languages cry wolf over the past 30 years, and approach new languages with an appropriate level of cynicism. For example, Cyclone, C#, and Go have all made claims to being a "better C" systems language, when in reality they never escaped academia (Cyclone) or aren't really playing on the same field as C (C#, Go).

So I'd suggest saying, "Hm, I didn't think of it that way, why do you say that?"

13 Likes

Ah yes. I came to that conclusion many years ago. I can see it in myself. I'm sure it's true of most choices people make in their lives. We can always construct a logical argument to support our choice, after the fact.

Getting proficient in a programming language does take a huge investment of time and effort. Not just learning the language but the libraries, the tools, everything around it. Having had to work in a dozen different languages over the years I can quite understand people being resistant to the idea of checking out yet another language.

Seems I have an deeply emotional desire for performance with safety in a cross platform language that I can use from servers to micro-controllers. So here I am going through all that learning pain again :slight_smile:

I'd feel even safer if Rust was available from multiple vendors.

I have very much appreciated all your essays. Very informative.

3 Likes

We are trying to write very loosely coupled C-modules.

This typically results in a lot of pointers for dependency-injection and also classic callback pointers.
This then typically results in a lot of runtime checks of the pointers not being NULL.
So (for us) sufficiently decoupled C-modules costs

  1. RAM for the pointers
  2. Opcodes (=Flash) for the pointer checks
  3. The execution-time to do that.
    Additionally:
  4. coding like that makes it harder for the optimizer to find out what's going on between modules = Flash cost

So the awareness of these four costs for us, makes (the least fearful) people think.
So, all the 'zero cost' abstractions is a key feature also!

(So why not go C++? ... don't even get me started ... :slight_smile: )

1 Like

Interesting. I have only ever seen one embedded project pull that off. They did it really nicely too. I was happy to borrow components they had created in other projects we had going on. I recall the manager of that project saying it was hard work getting the devs to maintain that discipline and it took half a year to get new hires to even get the idea, if if they knew C well. C++ was still new at that time and nobody in the embedded world had heard of object oriented programming or 'dependency injection'

Those projects did not have stringent speed demands and I guess they had space enough with the 68000 processor they were using.

1 Like