DEV Community

Cover image for Is modern C better then Rust?
David
David

Posted on • Updated on

Is modern C better then Rust?

In the past years we have witnessed probably the fastest adoption of a new language, Rust. This article explores the question "Are we moving too fast?".

A little background about me, I started programming in Java, then I learned c++, got bored so I learned Rust and at the end, I learned C. Rust was the first programming language that I really felt like I like it, but one of my friends told me that I should also give C a chance. In less than a month after starting out, I've started rewritten the project I was working on from Rust to C.

This project showed me that there is more to this story than it seems. For example, today, I was searching reasons why use Rust over C, and what I found shocked me. There was an article that said that you have to explicitly destroy every object(I mean even the integers allocated on the stack) in C, no wonder everyone thinks C is unsafe.

And there is the whole thing with using Rust in the Linux kernel which seems questionable if you ask me. The reasons for my thinking:

  1. The build time would turn into hours.
  2. Rust has too many abstractions by default.
  3. It depends too much on the standard library.

The main argument to using Rust over C is safety, but if you want there are some very useful macros and libraries out there that can your code much more reliable.

Many say that C is obsolete because it doesn't have templates/generics, private member variables or namespaces. I disagree because with a bit of creativity, you can actually achieve all of these, more or less in C. Speaking of creativity, when I'm coding in C, because of the limited features, I am forced to write much more creative code that in C++ or Rust.

And as a last note, I recently explored newer programming languages like zig, and that made me realise that one of the best thing about Rust is that is has amazing docs. If you want to learn something about C and use the web for info, you'll find like 4 different sites that explain the same thing with different formatting, which might be confusing for a person who is just starting out.

P.S. I've edited this post to rewrite some sentences.
P.P.S. I am very sad that dynamic arrays didn't catch on.

Opinions are welcome in the comments.

Latest comments (12)

Collapse
 
pylover profile image
Vahid

After learning a lots of programming languages with 28 years of experience, I just picked up 3 of them, C, Haskell and Python.

Collapse
 
cosmin_sontu profile image
Cosmin Sontu • Edited

For me it's C, C# and F#. Of course after looking at many other languages and almost 20 years of professional experience (25 years of programming experience).

Collapse
 
keyhanjk profile image
Keyhan Jannat Khah ☕

YEES, I agree with you, rust is a good language, BUT there is more much than "safety", "concurrency",...
C is awesome, the beauty of C comes from simplicity and freedom, you can learn c in a few days but take years to be master in that. rust never beats c, in some areas, yes but in some others is not.
C language has soul and rust not. you can achieve all features in rust by c. although rust is a very good tool to reduce programmers' faults.

Collapse
 
pauljlucas profile image
Paul J. Lucas

Templates in C via macros is problematic for several reasons (see here among many other places) — which is why C++ implemented a true, first-class template mechanism. Also, errors are impossible to read since everything gets folded onto a single line. If you want templates, just use C++, not C.

Collapse
 
sucuturdean profile image
David

I forgot to add that clang and gcc tell you on what line in the preprocessor was the error which is so useful.

Collapse
 
sucuturdean profile image
David

And it's not that a programmer wants templates, somethimes you need templates for simple things like vectors, options and other. Templates in C++ aren't that good also because they created template meta programming which should be it's own language, not in C++ so that people can flex with their knowledge. The whay that they sould be is like generics in other languages.

Collapse
 
sucuturdean profile image
David • Edited

If you have preprocessor errors, you can ask the compiler to give you the preprocessed files that you can examine with your error codes. I understand that this is time consuming and that's why I made them if anyone needs them.

Collapse
 
pauljlucas profile image
Paul J. Lucas

Everything is not folded into a single line, that is just false ...

C11 standard §5.1.1.2 ¶ 1.2:

Each instance of a backslash character (\) immediately followed by a new-line character is deleted, splicing physical source lines to form logical source lines.

So a multi-line macro where each line ends with a backslash is folded into a single line.

Templates in C++ aren't that good also because they created template meta programming which should be it's own language ....

So offering flexibility by allowing (but not requiring) template meta-programming makes them bad? And it's not clear what a template-meta-programming-only language would be useful for.

Thread Thread
 
sucuturdean profile image
David • Edited

Well what I meant with template-meta-programming-only language is something like the preprocessor in c and c++, but with type checking. Like that you can see what the output of templates is and if you don't know template meta programming still understand what is going on(plus it could be useful if it was made to work with multiple programming languages). That claim about C11 standard it's true and I am sorry for the fact that I said that it's false, I should have researched more into it before saying it's false(I edited that part out so that it doesn't spread fake news).

Collapse
 
thepuzzlemaker profile image
James [Undefined] • Edited

I have a few gripes with the content of this post:

I bet that almost no Rust programmer can tell me what enums look like in memory

Rust will likely never have a stable ABI. This is why there is #[repr(C)]. With #[repr(C)], I believe you cannot represent tagged unions with enums, but they at least have a defined size and padding as to be compatible with C's ABI (or more technically, the ABI of the underlying platform) There's also #[repr(u{8,16,32,64})] which allow you to define the size the enum is defined as, which is something I think you can't do in C.

(Not being able to know what the assembly would look like by just looking at the code)

You can't even do this in C. C compilers have hundreds of incredible tricks they have up their sleeve to try and make your code be efficient. If you're running on any type of optimization, which you are going to end up using in production anyway, it's almost guaranteed the assembly will not be what you expect, perhaps in a very different way.

Codegen systems are designed not only to make code efficient, but also to minimize instruction size which ends up going to binary/library size. That's why you'll see things like xor eax, eax instead of mov eax, 0. It doesn't seem like it, but the latter is 3 bytes shorter. And when you have millions of instructions in a large program or library, those bytes add up.

But anyway, why would knowing what assembly would be generated matter anyway? If you need to code in assembly, code in assembly.

Many say that C is obsolete because it doesn't have templates/generics, private member variables or namespaces, but with a bit of creativity, you can actualy achieve all of these, more or less in C.

Sure, that's true. But in Rust these things are first-class. You don't need creativity to achieve them, because they're already there for you. Generics in Rust work almost exactly the same as they would in C: monomorphization—generating new code for each type instantiation. This has its downsides (such as bloated binary sizes sometimes), but it's great for optimization and simplifies other parts of the compiler a bit too.

When you're hanging on creativity, it means things will go wrong at some point. Creativity isn't infallible—and I'm not saying Rust is either, it's had its fair share of bugs and certain design decisions that maybe weren't the best—but when you're relying on people pretty much reimplementing something every time they need it (in this case, generics macros), perhaps with new bugs each time, you're bound to run into problems.

Rust provides more safety guarantees with this. By having generics, templates, namespaces, private member variables, etc. in the language itself, it ensures that you can't bypass things by accident (or on purpose) and do something dumb. It also offloads the immediate complexity from the user to the compiler. Sure, say what you will about lifetimes in Rust, but it's much easier to deal with the compiler yelling at you than it not and having silent bugs that you never notice until they become a serious problem (read: security vulnerability).

I want to talk about refrences in Rust. While they are very safe, they are the devil if you are working low-level.

What are your sources for this? References in Rust are entirely compile-time. There is no overhead for references, perhaps excluding fat pointers (for slices, dyn Trait and other unsized types). And you have the unsafe escape hatch with raw pointers if you need to implement something that can't be expressed with the borrow rules of Rust, but that you know is safe.

[Y]ou still see [Rust programmers] on the internet saying that you sould rewrite C code in Rust

This is a problem in the Rust community, frankly, though it's getting better. For small projects, sure, that may be beneficial if they actually have large use. But I mean small. Where people should be asking for Rust should not necessarily be for old projects, but for new ones. There's an opportunity cost here—the cost of porting to Rust, versus the opportunity of the safety you'll get. For many projects, that's just not worth it. And that's why it's better to try to get Rust on the new frontier: projects that would be written in C but someone suggested Rust (kindly) to the developer or the developer found out themselves.

That's why I think Rust in Linux is a great frontier. There's almost no chance Rust will replace any core kernel code in Linux. But it's a great idea to have modules, which are usually third-party code, with fairly high-level access, to use a memory-safe language like Rust. Doing just that can probably mitigate a number of security issues. But yes, it is infeasible to replace C in the core kernel with Rust. It would stick out like a sore thumb, and yes, having more Rust in the kernel would probably increase build time.

But modules are optional. Don't need a module? Don't build it!
Don't want all the code bloating up your kernel image? Just don't include it with the image. Have modprobe deal with it later!

There's still a lot of work that needs to be done with Rust and its community, yes, but I think that there's a lot of misconceptions that need to be solved first.

The conclusion to this article I think it is that you should try something before you say its bad.

I agree with this sentiment. And I think it goes both ways. If you have used Rust and never C, perhaps try C. If you have used C and never Rust, perhaps try Rust. Rust isn't a fit for everyone. So is C.

But I think that it's not helpful discounting the idea of Rust with claims that are partially factually incorrect or just not helpful towards your argument. Don't get me wrong—you're allowed to have your opinions, and I'm allowed to mine. But when your opinions are partially based on misconceptions, perhaps it's time to rethink them.

Please let me know if you think anything here I said is factually incorrect, or simply unproductive; I don't intend to be a hypocrite in what I'm saying. Some of these thoughts are off the top of my head, so it's possible some things may have been stated wrong.

Collapse
 
sucuturdean profile image
David

First of all, you still didn't say what Rust enums look like in memory, so I think that point still stands(I didn't say that they are not stable). Second of all, it never happened to me to have such code that doesn't resemble what I wrote, but from what I heard it can happen from time to time. If you have a problem like this then a good solution would be to search what things the compiler might optimize. I strongly disagree with this sentence: "If you need to code in assembly, code in assembly.", writing in assembly takes too much time and is even more error prone than C. C was made to be a portable assembly. I know that having a language feature is very nice, but what I am saying is that you can still do that in C. Many people say that they don't use C for this reason and I wanted to show them that C is not as obsolete as they think. I didn't say that references are slow or bloat, I said that the way they work doesn't really fit in the low level programming. When I did some low-level programming in Rust I felt like I was spending more time on how to use refrences(not learning, just integrating them in the project) then the project itself, tho I can see why they are very useful for making desktop applications and stuff like that.

Collapse
 
thepuzzlemaker profile image
James [Undefined] • Edited

First of all, you still didn't say what Rust enums look like in memory, so I think that point still stands(I didn't say that they are not stable).

The part about not being stable is the important part, as it's not really possible to know exactly how a (#[repr(Rust)]) enum looks without looking at the compiler. (And even then, layouts can still be randomized as Rust makes no guarantees about the layout of structures and enums in #[repr(Rust)], though it can guarantee that for #[repr(C)].) Generally, for unions without data, it's a byte. For unions with data, it's a tagged union with a byte for the discriminant and the data as an inline union. Note that as padding is not strictly defined, it may be arbitrarily bigger to align by platform-specific aligning rules. I don't know what they look like in #[repr(C)] but I'm sure it's somewhat similar and probably is in the Nomicon.

An unstable ABI for enums makes it really nice for things like Option<&T>, because references are internally pointers (at the machine code level), so None can be represented by a null pointer, so it only takes up the size of a pointer, not the size of a pointer plus one. This single byte can add up and also due to alignment it may be padded to 2, 4, or even larger amounts of bytes which affects performance and memory usage.

Second of all, it never happened to me to have such code that doesn't resemble what I wrote, but from what I heard it can happen from time to time. If you have a problem like this then a good solution would be to search what things the compiler might optimize.

Sure, that's fine and probably is a normal experience. I was kind of exaggerating, I guess. But still I don't think it's really necessary to know what your code will look like at the low level, as long as that low-level code is just as fast as you would expect. And Rust, for the most part, is. (There's still a lot of improvement to be done here, though too!)

I strongly disagree with this sentence: "If you need to code in assembly, code in assembly.", writing in assembly takes too much time and is even more error prone than C. C was made to be a portable assembly

Sorry, that was phrased badly. What I meant is that if you need the precision of specific assembly instructions, perhaps you should just use assembly.

I know that having a language feature is very nice, but what I am saying is that you can still do that in C. Many people say that they don't use C for this reason and I wanted to show them that C is not as obsolete as they think.

I absolutely get that! C is by no means obsolete, and it probably won't be for a while; but there are still new features that are in other languages like Rust that add on to C and make it (relatively) easier and much safer to write low-level code.

I didn't say that references are slow or bloat, I said that the way they work doesn't really fit in the low level programming. When I did some low-level programming in Rust I felt like I was spending more time on how to use refrences(not learning, just integrating them in the project) then the project itself, tho I can see why they are very useful for making desktop applications and stuff like that.

Sorry, I misunderstood what you meant as you didn't explain on that. I feel that the way they work absolutely works for low-level programming because it allows you to have fairly precise access over memory while not sacrificing memory safety. There are only a few places where you need to use raw pointers in Rust over references, and most programmers in Rust, even lower-level ones, I'd say, very rarely have to use raw pointers. Especially because there are APIs over pointers like Pin that allow certain features with unsafe but without the error-prone raw pointers.