<center> The Rust Journey, Part 0: Why Systems Software and Why Rust? === *Written by [Thomas Lisankie](https://tomlisankie.com). Published 2021-06-22 on the [Monadical blog](https://monadical.com/blog.html).* </center> I’ve been curious about building systems software for some time now. I use systems software constantly as a web developer, without ever thinking about whether it will work or not. Databases are black boxes to me--I don’t think about how web servers handle an HTTP request so incredibly fast, and it usually doesn’t matter to me how AWS’ hypervisor works. So long as it works and so long as the interface stays stable, I don’t need to think about how it actually does this. I’ve been increasingly drawn to these systems over the last couple of years. Distributed systems (especially decentralized ones), networking services, databases, and lower-level software in general have been particularly interesting to me. The first three examples blow my mind because of how robust they are, despite the giant loads they process on a daily basis. The latter interests me because it allows the developer to take fuller advantage of the hardware they’re programming for and offers the flexibility to both hang yourself and optimize performance to your heart’s content. Until recently, I hadn’t really taken the time to satiate my curiosity and really dive into learning about and beginning to contribute to and build these systems. But now I’m thrilled to say that I have the opportunity to make it a higher priority! To start learning about and building systems, though, it's not enough to just read about how these systems work. I have to work on tinkering with and building my own (toy) versions. And to do that, it helps to know a programming language that’s appropriate for systems development. But before we talk about languages, what’s systems software in the first place? ## What even is systems software? First of all, a note what sorts of systems are we talking about when we say “systems development”? If you look around the web, you’ll find that [everyone](https://en.wikipedia.org/wiki/Systems_programming) [and](https://channel9.msdn.com/Events/Lang-NEXT/Lang-NEXT-2014/Panel-Systems-Programming-Languages-in-2014-and-Beyond) [their](https://willcrichton.net/notes/systems-programming/) [mother](https://softwareengineering.stackexchange.com/a/151627/327366) [seems](https://softwareengineering.stackexchange.com/a/151615/327366) [to](https://softwareengineering.stackexchange.com/a/399647/327366) [have](https://news.ycombinator.com/item?id=21732987) [a](https://news.ycombinator.com/item?id=21734228) [different](https://news.ycombinator.com/item?id=21734576) definition of what systems programming is. For the purposes of this article, I’m going to define systems software as software with a fairly stable interface that needs to have low resource overhead, and is mostly used by software developers who are creating applications on top of it. So, under this definition, examples of systems software include (but are certainly not limited to) databases, certain command-line utilities, web servers, distributed computing projects, blockchain networks (they are often built in systems languages because of the need for speed), graphics libraries, compilers, OS kernels, hypervisors, and container runtimes. What these projects tend to have in common are the need for stable interfaces and maintenance over time, the ability to have fine-grained control over lower-level systems and hardware, and speed. All of these feed into each other in one way or another--let’s try and tackle them one by one. ### Stable Interfaces An interface is the way a consumer of a piece of software talks to that software. It’s how someone developing software outside of the codebase interacts with the functionality of your component. And a *stable* interface, as the name implies, is an interface that will stay essentially the same over the course of a long span of time. Stable interfaces are essential in systems software specifically because other people are building (sometimes massive) software projects on top of them. Imagine if the words of a language changed somewhat significantly on a daily basis. That wouldn’t be a language that you could communicate or build social relationships with--you can’t build for the long-term on shaky foundations. In addition, the further down the stack you go, the more stable the interface has to be. This is because the more changes that happen to the interfaces further down the stack, the more layers further up the stack could break. Really far down on the stack is where you usually see formal standards. <center> ![You cannot build a house on a weak foundation](https://docs.monadical.com/uploads/upload_95b6f75c3b4780475fc3c6629ac2c12b.jpeg) </center> It’s not just the way other software interacts with the system that needs to be stable, systems software needs to be around and maintained for a long time. What happens if the software an application developer has been relying on just stops being maintained and gets discontinued? This isn’t *as* big of a problem these days with modern open source, but it’s still an issue. Users certainly don’t want to have to get into the weeds of what’s going on in the Postgres dev community in order to join two tables. The interface should stay the same and the implementation should be maintained to fix bugs and security holes, and to introduce performance improvements. ### Speed It’s essential that systems software has speed. The slower systems software is, the slower the application software of *everyone* who uses it will be. If Postgres takes an extra second to run a common query, that’s an extra second for every single project that uses Postgres. Sure, an extra second isn’t much and most people probably won’t even notice. But systems software needs to be useful to as wide of a breadth of consumers as possible. This goes for everything from a weekend project to a mission-critical business application. For the latter, the speed requirement is crucial, and for the former it’s a huge plus. Also, it’s not like Postgres will be the only code running in someone’s project--they’ll also have all sorts of application-specific code that takes time to run. Systems software, therefore, should be as fast as possible. Let slow code be the fault of the application developers. :) ### Fine-grained control over resources Systems software should have a small resource footprint compared to other types of software. This isn’t even close to as important as it used to be, because of how powerful even commodity hardware has gotten ([look at the specs of the iMac from 20 years ago](https://en.wikipedia.org/wiki/IMac_G3#2nd_generation:_Slot-loading) compared to the [specs of the most recent model](https://en.wikipedia.org/wiki/IMac_(Apple_silicon)#Specifications), [a timeline of hard drive capacity](https://en.wikipedia.org/wiki/History_of_hard_disk_drives#Timeline), etc.), but I would still say that it’s important on principle alone. If you’re developing systems software that will be used by others, they’re putting some amount of trust in you that your code won’t be a resource hog (relatively speaking at least) and that you’ll minimize resources required whenever you can. Pigging out on resources should have nothing to do with some dependency, and everything to do with that developer’s own code. ![What a difference 20 years can make](https://docs.monadical.com/uploads/upload_3b071958b0f526f83239b187b1e199b8.jpeg) If you’re going to make sure to not be a resource hog, you need to be able to choose which resources live and die over the course of the lifetime of your program. And if you’re relying on a runtime (somebody else’s code), you no longer have fine-grained control over resources even if your code is maximally efficient. Whatever decisions that runtime developer made may not be in the best interest of you, the person trying to develop systems software. <center> <img src="https://docs.monadical.com/uploads/upload_2109af39804e3ae2fecd1d80e07304cd.png" style="height: 300px; width: 400px; margin: 10px"/> </center> ## What languages are suited for systems development? Technically, you can build any computer system in any Turing-complete language. However, that doesn’t mean it’s a good idea to, say, implement an OS kernel in Python. Besides the fact that Python needs a runtime with a garbage collector to operate, there’s also the fact that for every instruction that a C++ program executes, [the corresponding Python program may have to execute 10-15](https://www.stephanboyer.com/post/38/counting-to-a-billion) (if using the [fastest Python runtime around](https://www.pypy.org/)). These quickly build up, and many Python programs will be noticeably slower than their C counterparts. This doesn’t matter in practice for many applications these days, so Python is often absolutely fine. For systems software, it’s important that we be able to access and manipulate low-level constructs. In addition, the number of instructions generated by the compiler per statement in our codebase should be minimized. Both of these are mandatory for performance and flexibility reasons. ![Compilation vs. Interpretation](https://docs.monadical.com/uploads/upload_94ab4959d6a8e291937b135b2026b8a3.png) It’s also important that the language we’re writing systems software in is compiled as opposed to interpreted. This is because with an interpreted language, each line of code needs to be read, transformed into an AST, transformed into some bytecode, and then finally transformed into machine code and executed. Contrast this with a compiled program, where the reading, transformation into an AST, and translation to machine code all happen at compile time. Once runtime comes around, the only step involved is running the machine code. Thus, a compiled language will usually be faster than an interpreted language. Another important characteristic of a language that’s good for writing systems: backwards compatibility. Code I wrote 5 years ago should very, very rarely break with compiler updates. If new versions of the language have come out (keywords have been added or syntax has been changed, for example), I should be able to specify to the compiler that I’m using an older version of the language and have everything still compile. Some languages that immediately come to mind are C, C++, Rust, Go, and Java. The first three are compiled directly to machine code and have no runtime required. Go is compiled to machine code but has garbage collection code compiled along with the application code (it’s not this simple and, as far as I know, the Go compiler will throw away whichever elements of the garbage collector aren’t necessary for you). Java is compiled but to bytecode instead of machine code. That bytecode then gets fed into a Java Virtual Machine (JVM) and executed there. With C, C++, and Rust, you can write essentially any systems software and meet all of the requirements we outlined earlier. With Go and Java, you can viably write many, but not all, of the example applications from earlier. Writing a hypervisor in Go wouldn’t be too great because even though Go might have a very efficient garbage collector, it will lose out in performance every time to a carefully-written C program. Writing a command-line utility in Java is also probably a pretty bad idea since, whenever you want to use the tool, a pretty huge chunk of the time it takes to run will be dedicated to booting up a JVM instance. So if we want to learn about systems software, what it takes to develop, and be able to have very fine-grained control over how resources are allocated, we’re going to choose from C, C++, and Rust. ### Why Rust instead of C and C++? #### First, a note on Assembly Before diving into this, I’d like to make a note on a family of languages that come up in conversation when writing low-level systems: assembly languages. Assembly languages are the lowest level of abstraction one can have before hitting pure machine language. In assembly languages, there are different instructions for specific operations that can be performed on the computer’s hardware. For example, if a developer wanted to move the number 123 to a register called “eax”, she would give the instruction “mov eax, 123”. This is incredibly flexible and simple, but there are usually many such instructions needed to perform tasks that are only a few lines in C. Imagine having to write a modern query engine in Assembly. The level of tedium you’d have to tolerate can hardly be overstated! If you don’t trust the compiler to make the most optimized version of some code for you or you *need* Assembly for whatever reason, use it. #### Now, back to our regularly scheduled programming... So, why Rust instead of C and C++? Trick question. It’s not really Rust vs. C / C++. It’s more like Rust with C vs. C++ with C. Why is this? ![rust vs C++](https://docs.monadical.com/uploads/upload_68935e31c49e72858eb55902621c5891.png) Rust and C++ are both focused on essentially the same issue: how can we have nice abstractions and really nice language features without sacrificing speed and the ability to manage low-level details? How can we have zero-cost abstractions? How can we have our cake and eat it too? Rust and C++ both think we can have nice things. It’s just a matter of where to put the complexity of transforming the nice abstractions into machine code. C says: “I don’t care about having nice things, I’m okay with living out of a suitcase. If you want those nice things you can build them with me though!” Interpreted languages like Python say: “We have nice things, but we’re going to handle them at runtime so things are going to move a bit slower around here.” Rust and C++ say: “How about we have nice things and handle them at compile time instead? That way the programmers are happier because they have nicer tools and the users are happy because they don’t have to wait as long. The only sacrifice we have to make is having longer compile times and a more complex compiler.” Why Rust instead of C++? The problem with C++ is that while you do get a lot of nice tools (though some would say these are a bit too complex), you’re still open to all of the same classes of bugs that C has. So you have a steep learning curve, but you end up dealing with all the same kinds of bugs as C (maybe even more since there’s so much more complexity involved). Rust eliminates almost an entire class of bugs that have plagued C and C++ developers for decades: memory safety issues. Rust also has a fantastic ecosystem of first-party tooling surrounding it. It has a package manager and build tool (Cargo), tools for updating the compiler version with a single command, and a code-formatting tool. With C++, this first-party ecosystem simply doesn’t exist (none of these were really expected back when C++ first came out, so it’s not exactly surprising). And why learn C? While I don’t think I’ll end up using it much in conjunction with Rust, C is the lingua franca of systems programming, so a large number of examples of various concepts that come up in systems programming are written in C. C is also just a layer of abstraction built directly on top of Assembly, conforming itself somewhat to the structure of Assembly. Assembly is basically there to answer the question, “what sort of language would exist if I wanted to tell the computer *exactly* what to do with specific bytes and memory addresses?” So if you’re writing code in C, you’re getting a very good intuition of what’s happening directly on the hardware with each statement. Since I’m new to systems programming, it’s important that I get a feel for the abstractions that will be directly beneath my feet. Learning C will also help me appreciate all of what Rust does for me. #### Safe-by-default and unsafe-by-choice is just better #### It opens up low-level systems programming to new demographics The human mind can only store and simulate so much information in working memory at once. The amount different people can store varies quite a bit (different [person-byte](https://youtu.be/A3AdN7U24iU?t=1265) sizes) and it takes people who not only have a high person-byte size but a high tolerance for dealing with tedium to handle and remember to write safe C on a continuous basis. This is a fairly small subset of developers. I'd be willing to bet that, on average, Python programmers (defining as people who almost exclusively use Python when they code) have a shorter attention span and worse memories than C programmers. This isn't because Python programmers are stupid, it’s because writing C (at least safe C -- possibly this isn’t true if you just don't care) on a regular basis attracts and rewards people who can do all the extra things that C forces you to do when programming with it (which require a longer attention span and bigger memory working in tandem). But what if most of those memory safety issues were handled up front and dealt with at compile time? That would open up this sort of programming to entirely new demographics of developers who have had experiences in areas that these veteran C programmers have never touched. What sorts of software may get built that otherwise never would have? #### We’re professionals and we should be using the best tools for the job As professionals, our job is to make sure that a project not only gets done but gets done correctly and reliably. We should know our tools well and apply the right tool for the job. Not only all that, but when a superior tool comes along that improves significantly on the status quo, we should learn it and transition to using it when possible. In this case, Rust is a significant improvement on the status quo of low-level systems languages. In situations where we would normally reach for C and C++, we should try and reach for Rust in the future. #### But shouldn’t we just be more careful...? I don’t care how careful a developer is in her work. If she’s human, she’s prone to errors. And it just so happens that those errors can end up having a myriad of terrible consequences. If a tool comes along that prevents her from making those errors from the start, she should aim to start using it. ![Why Rust](https://docs.monadical.com/uploads/upload_9cbd454e18d918b11b5eed120188219a.jpeg) [Trains used to have to be stopped by a multitude of brakemen who would run around on top of the train cars having to crank on the brakes for each individual car.](https://youtu.be/A3AdN7U24iU?t=185) This usually worked but was prone to quite a bit of human error, the worst of which had fatal consequences for the brakemen and passengers. Was the solution to continue using this unsafe (and inefficient) brake system or to [replace it with a system that was heavily automated and significantly decreased the number of train accidents](https://auto.howstuffworks.com/auto-parts/brakes/brake-types/air-brake.htm#pt1)? Obviously the latter. Use the better, safer tool that helps you be better at your job. ## What’s next? In my next post, I’ll give a more in-depth view of *why* Rust is the way it is. I’ll explore some examples in C where bugs could creep in that Rust would prevent, and I’ll give an overview of how the Rust borrow checker prevents these bugs. With the “why” out of the way, it’s time to take the first steps of our journey! --- <center> <img src="https://monadical.com/static/logo-black.png" style="height: 80px"/><br/> Monadical.com | Full-Stack Consultancy *We build software that outlasts us* </center>
- Demystifying Databases: Cracking the Code of Efficient Data Storage
- To Fine-Tune or Not Fine-Tune: Large Language Models for AI-Driven Business Transformation
- 50 Shades of Rust, or emerging Rust GUIs in a WASM world
- Filters with Github Actions
- View more posts...
Back to top