Comment by burlesona
3 years ago
This looks like an awesome resource, thanks for sharing!
Question: at the point my impression is that Rust is the consensus best overall systems programming language (assuming you’re starting something new). Is that just me or so others share that perception?
I’ve mostly done high level programming in my career but have wanted to eventually move into some barebones hardware stuff for fun. So I’m curious if Rust really is the best way to go these days (versus my old assumption that I should try to master C).
The other commenters already mentioned it but Rust is an alternative to C++, with a similar approach to "zero-cost abstractions". Meaning it provides you with the means to do systems programming at a higher-level and ignore low level details.
If you're learning low-level/barebones hardware programming, this is both a blessing and a curse. If you already know the low-level stuff, then Rust/C++ abstractions let you organize your code in a much nicer way, but if you're learning the low-level stuff, then those abstractions are actually in the way.
C won't get in the way, for better or worse, so it's definitely worth learning, at least to appreciate the safety valves Rust and C++ give you after you've repeatedly shot your own foot (specially Rust which learned from most of the historic mistakes of C++).
If you don't want to deal with C-nonsense, then Zig is probably the best alternative for that sort of low-level programming, it even has freestanding (no operating system) as a first class target, stack-traces and all.
FWIW you can easily write Rust in a C-like style, using primarily free-standing functions and structs. You can opt-in to “advanced” features (traits, enums, generics) as you see fit.
Interfacing with other libraries, is ofc, a different matter, but you can usually wrap the library API in your own functions to minimize this mismatch.
All of Rust's functions are available as "free-standing functions" because of how types work in Rust, and Rust doesn't have classes, just structs. So, as a consumer nothing changes.
... is perfectly legal Rust, it would just be more idiomatic to write:
The availability of all functions as "free functions" is convenient when you need, say, a filter predicate, since of course std::collections::HashSet::is_empty is exactly what you wanted if what you wanted to express was the predicate "is this HashSet empty?" and in languages that aren't allowed to do this you'd need to pointlessly shuffle chairs around to achieve the same thing instead.
2 replies →
You can, but it's not like the Rust std-lib is in that style, nor does it play well with the ownership semantics, so my point stands.
You can also write literal C and use the C std-lib in C++ but it is the same problem. You're not using C++ at that point
4 replies →
In addition to what other people have said, I do want to make this point: Rust (more specifically the unsafe core) probably works slightly better as a "portable assembler" than C does. For example:
* Rust doesn't have volatile variables, you use volatile intrinsics on the loads and stores.
* C declares that arithmetic on 'char' and 'short'-sized variables gets promoted to 'int'; Rust actually has proper u8/u16 arithmetic support.
* Rust supports something akin to multiple return values via tuples, which means you can actually get operations like checked-overflow arithmetic supported in the core language, unlike C.
Rust's Wrapping integer types even behave the way "integers" natively work in your CPU rather than, as most programmers want and most languages try somewhat to deliver, behaving like the integers you learned in school on that infinite number line.
Even though this looks like it must surely be some frightfully complicated object-oriented nightmare, Rust's types only exist at compile time, so at runtime (my illustration was constant, but with real variables) this would just to be a 32-bit register doing normal CPU stuff. The optimiser is like "Yeah, Wrapping is how the CPU works anyway" and gets on with it.
Now, on one hand, this seems like a very clumsy thing to write. But then on the other hand, did you actually want CPU-style wrapping integers? No? Then the ones you actually did want are easy to work with in Rust, but don't kid yourself you wanted a "high level assembler" if you can't even handle modulo arithmetic.
> at the point my impression is that Rust is the consensus best overall systems programming language (assuming you’re starting something new). Is that just me or so others share that perception?
I think I'd probably agree with that impression. It's the route I've taken into "systems programming", and I don't regret it at all. The big advantage of going Rust-first for me is that C and C++ have so many unspoken rules that you need to follow in order to avoid security issues and hard to debug errors, whereas Rust codifies most of those as compiler errors. That makes it a lot more accessible for the beginner to learn not just the basic syntax, but a best practices and good habits.
C and C++ are still the mainstream at the moment, but I think they've peaked and would expect their popularity to wane over the next 10-15 years. On the other hand, Rust has only just hit the mainstream in the last year or two, so there's still a few missing pieces and adoption is not yet that high, but I think it's by far the best intro low-level language overall.
> The big advantage of going Rust-first for me is that C and C++ have so many unspoken rules that you need to follow in order to avoid security issues and hard to debug errors, whereas Rust codifies most of those as compiler errors. That makes it a lot more accessible for the beginner to learn not just the basic syntax, but a best practices and good habits.
Yes, I agree this is the big advantage for me too. Rust allows mediocre devs to build more ambitious systems than they would have otherwise attempted in other languages. You can certainly build anything you want in C++, but from my experience due to the number of, as you put it "unspoken rules", novice developers will encounter a lot of foot guns. It leads to code that works but is very brittle; if you look at it the wrong way, it ends up segfaulting.
Rust says "You can't run this until we're sure it's not going to violate any of my assumptions of how a system is built" and it goes through your code with you to check off all the boxes. Is this thing mutable? Does it have more than one owner? Yes? Well then Rust says that's going to lead to pain in the future and prevents you from doing it. C++ will let you do it and hope that the learnings from the pain you encounter in the future due to your poor choices will prevent you from doing it again.
When I started learning Rust in 2015 the biggest thing in C++ I had built was a robot, and in that world you do most of your work as message passing. It's really more like a style that Erlang devs would find familiar. It's really not equivalent to doing systems programming in the OS/compiler sense. I found Rust very hard to use because I had poor habits in terms of object lifetime and ownership management. But over time the I figured out what the borrow checker wanted and in doing so, it made my code sounder, and therefore far more robust than what I would have put together in C++.
Going forward I apply these ideas to all languages I write in, so this is why I teach Rust in my PL course: even if students aren't going to write Rust in their future career, I've found it makes them think harder about variable lifetimes when they switch back to C and C++ in the OS course.
I don’t think “best” means anything objective, and “systems” is also pretty ambiguous (it describes a class, but that can be interpreted extremely broadly). Rust is still fresh, and people like new things. I think it has legs, but practically I think things need the test of time and many critical projects before you can really assert that it’s the “best”, if at all.
In terms of playing with hardware: did you want to learn rust, or understand low-level programming? I think rust will help you make a more robust, correct application (with a lot of time and effort understanding rust itself), while C won’t do much to help you but you’ll be able to be right next to the hardware. Personally, I think if your goal is to understand how hardware works, I’d use C. (But if you want to learn rust, use rust.)
C is valuable because you shoot yourself in the foot over and over. Once you’ve dealt with use-after-frees, segfaults, memory corruption, then you will really appreciate what Rust does for you. Sure, you can learn Rust up front, but I on,y appreciated it after I had spent some time acquainting myself firsthand with the problems it’s trying to solve. Just my opinion though—Rust is a ton of fun.
C is valuable because it puts an emphasis on ABI. In that sense, it’s a gentle introduction to assembly - functions map 1-to-1 with generated subroutines (modulo inlining), and no monomorphization/templating encourages code reuse at the instruction level. Those philosophies are important stepping stones to understanding concepts like dynamic linking, calling conventions, how registers behave, etc.
Of course, you could emulate this behavior with Rust using extern and unsafe and whatnot, but the affordances of Rust steer you away from those details in favor of the higher level abstractions it offers. Which is what you want for 99% of software development, but when you’re writing platform specific code (e.g. OS startup, context switching, optimized SIMD) that a compiler can’t reliably generate, it helps to be able to quickly prototype something with C and then tweak certain instructions in the generated assembly subroutine until you get what you want.
You can write Rust in a C-like style, without generics and without traits. The output assembly will likely match the C output too.
My perspective was kinda the opposite: I'd already heard of use-after-frees, segfaults, memory corruption, etc. And I was very glad that I didn't have to deal with these as a programmer in high-level languages (JS, etc). As such, learning C was pretty daunting, knowing that even expert-level C programmers tended to hit into these issues, and there seeming to be 1001 rules that one has to learn to avoid them.
Rust provided a way into low-level programming without having to deal with any of these things at all. All I had to do was learn a few concepts like allocation and ownership. Easy-peasy compared to the above! This was especially true as when I learnt Rust I had need of it in production at work. I would not have had the confidence to put C code into production as a novice C programmer with no oversight, but with Rust this wasn't a problem at all.
C++, Ada, and Rust would all be good choices if you want to use something other than C. The decision to me would come down more to the ecosystem.
C++ has in the last decade provided measures for memory safety. Online resources are plentiful and references are easy to get at a bookstore. There are many different compilers (a plus I'd say compared to the monocultures many languages have), but Clang and G++ are ones to look at in particular. They share many extensions to C++ which are useful in systems programming. Clang can be installed on Windows using MS Visual Studio, while G++ is Unix-like only. C++ has many different build systems to choose from, but CMake a common portable one.
Ada came from a similar time as C++. It focuses less on memory safety than Rust, but has more of a focus on overall program correctness. A subset of Ada, SPARK, can be formally verified. The language's culture has more of a focus on embedded systems than general systems programming, and has less of an online presence than the other two. You will have to use reference materials and books more than online guides compared to Rust or C++. The open-source compiler of note is GNAT, it supports both Windows and Unix-like systems. It comes with a build system.
Rust, as you probably know, has a huge focus on memory safety. Rust is in culture much more like newer languages such as Python. Forums are active, updates to the compiler are more frequent, and online resources are plentiful. The book describing the language is a living document available online, unlike the other languages listed. The compiler, build system, and package management system is the same for every supported platform. This to me is a big plus, though package management is not very important for kernel-level development.
“Best” is really subjective and depends on your goals. It’s certainly a nice language, but it’s really a modern take on C++ rather than C. Zig is worth a mention here: it’s a modern take on C, and it tries to avoid the complex features that Rust uses to prove safety at compile time. There’s a contingent of people who don’t like the complexity of Rust or C++ but want to move away from the C footguns, and they normally become Zig fans.
That said, C is still a perfectly fine, very mainstream choice. If you just want to learn systems programming (rather than simultaneously learning systems programming and a new language), it might be the right place to start.
Just to add to this comment, sometimes (often) the Rust's documentation(s) will point out footguns in C which is a valuable resources in & of itself.
I'd compare Rust more to C++ than C. It's a bit higher in terms of abstraction, though still relatively transparent about what happens. It avoids doing anything implicitly, so when a value is cast to another type or memory is allocated it's visible in the code.
It's not a fun language if you want to experiment or move quickly as the type checker might act as a brick wall in those cases. But as it prevents a whole class of bugs that are related to the majority of security exploits I think it's a pretty good choice if memory safety and performance are the main priority.
Coming from someone who is completely smitten with Rust, there are [edit: a few exceptional] "systems programming language" scenarios where Go is a much stronger candidate. e.g. minikube, lima, and $your_local_tool are probably better done with Go: the entire language is built around doing "shellscripty" things.
The concepts in Rust are a loose/spiritual superset of C; you'd be able to pick up C easily after learning Rust.
Also, I learned Rust with the linked series. It's extremely well though out and guides you into the mental model of "rustisms."
Do you possibly have a link to the series teaching Rust you mentioned. 'Rust linked series' doesn't bring up any results that look relevant, and I would be interested to take a look at it.
Probably “Learn Rust With Entirely Too Many Linked Lists”: https://rust-unofficial.github.io/too-many-lists/
By "linked" I meant OP
Long ago, I used to develop on systems where my program was the only code running. For example DOS, or TI calculators. Hard rebooting or yanking out batteries was a regular part of the process. I’m not sure, but I wonder if Rust could have greatly reduced the frequency of those activities.
There is no "best overall systems programming language", that is strictly an opinion. All you can really do is look at "by and large what do large minorities of users use as a systems programming language". Every language has its trade-offs.
I would say Rust is currently the best overall programming language, period. It is also a great systems programming language.
I spent several years writing C code, then a few years writing C++ and now moving into Rust.
My take: Coding in C will teach you a two unique skills:
First, mastering working with pointers will inevitably lead you to debugging cache-miss related performance issues, virtual vs physical addresses when dealing with MMUs and a couple of other low level CPU details that are hard to learn about any other way (yes, this can also be done with C++).
Second, Because C is a barebones language, you’ll have to build everything yourself. Linked lists, hash tables, queues, binary search trees - all of it! And since you are working with pointers, a lot of the data structures and why they matter will make sense at a level that is impossible to grasp with say python (For example, any C programmer who picks up the usual university textbooks on algorithms and data structures looking for a reference to implement a hash table will end up disappointed- most books tell you how hash tables work, but very few tell you how to implement a hash function correctly - and with C, this matters a lot!)
C++ trades off some of C’s language simplicity in exchange for developer velocity. Hash tables, vectors etc are all taken care of for you. But this is actually a dangerous trade off: C++ gives you a language that will not fit in your head, and you can never be entirely certain about the behavior of code hidden from you by design.
Rust makes a ton of sense as a C++ replacement. It takes the same trade off that C++ did (give up language simplicity for developer velocity) but also adds guard rails around to help keep things sane.
I am convinced that rust is the future in every place where C++ makes sense today.
C is different. Yes, it suffers from many of the same bad things as C++. However, people who write C also work differently: they are used to building everything from scratch, they know their projects need more time to complete, they can look at almost every single line of code and tell you roughly what machine code it will compile down to. The language is small enough that it everyone knows all of it, most agree on the best way to do things and critical bugs are often spotted by just recognizing that some code doesn’t appear to follow well known patterns for implementing something (see how the OpenBSD community find bugs for example).
In short, it’s hard to recommend rust over C because they kind of come with different developer ethos. But Rust over C++ is a no brainer any day. And Rust over C makes sense any time C++ over C makes sense (which is the case for most C projects)
One interesting quote about rust in the context of OpenBSD [1]
> For instance, rust cannot even compile itself on i386 at present time because it exhausts the address space.
C is not going away anytime soon.
[1] https://marc.info/?l=openbsd-misc&m=151233345723889&w=2