2.5. Rust and the borrow checker
In this chapter, we will learn about a unique feature of Rust called the borrow checker and how it relates to the concept of ownership. Compared to the previous chapters, this chapter will be quite brief because the concept of ownership and the Rust borrow checker are covered much more in-depth in chapter 3. Still it is worth giving a preview of this topic, as it is immensely important to systems programming.
The concept of resource ownership
In chapter 1, we learned that one of the key aspects that make systems software special is its usage of hardware resources. A systems programming language has to give the programmer the tools to manage these hardware resources. This management of resources is done through the resource lifecycle, which consists of three steps:
- Acquiring a resource
- Using the resource (technically optional, but if you acquire a resource you might as well use it)
- Releasing the resource back to the source it was acquired from
The operating system usually manages hardware resources, but in systems without one (such as embedded systems) programmers access the hardware directly. We will look closer at the different ways to access resources in later chapters. For now, we will focus on the consequences of this resource lifecycle. A direct consequence of the process of acquiring and releasing a resource is the concept of a resource owner. Simply put, the owner of a resource is responsible for releasing the resource once it is no longer needed. Without a well-defined owner, a resource might never get released back to its source, meaning that the resource has leaked. If resources keep being acquired but never released, ultimately the source of the resource (i.e. the computer) will run out of the resource, which can lead to unexpected program termination. As systems software often constitutes critical systems, this is not something that should happen. You don't want your airplane to shut down simply because some logging agent kept hoarding all the planes memory resources.
Different languages have different ways of dealing with ownership, from C's "You better clean up after yourself!"-mentality to the fully automatic "Room service cleans up for you, but you don't get do decide when"-approach of garbage-collected languages such as Java or C#. Rust has a very special approach to resource ownership called the borrow checker.
Borrow checking in a nutshell
The main problem with the resource lifecycle and ownership is that keeping track of who owns what can get quite complicated for the programmer. Languages that manage resource lifecycles automatically, for example through garbage collection, do so with a runtime cost that might not be acceptable in systems software. Here is where Rust and its borrow checker come in: Instead of figuring out which resources should be released at which point, Rust has a clever system that can resolve resource ownership at compile-time. It does so by annotating all resources at compile-time with so-called lifetime specifiers which tell us how long a resource is expected to live. There are certain rules for these lifetimes that are then enforced by the compiler, preventing many common problems that arise in manual resource management, such as trying to use a resource that has already been released, or forgetting to release a resource that is not used anymore. These rules are what makes Rust both memory-safe and thread-safe, without any additional runtime cost.
Unfortunately, we will see in chapter 3 that it is not possible to determine all resource lifecycles completely at compile-time, so there are cases where the borrow checker will not be able to aid the programmer. Additionally, the borrow checker has a very strict set of rules that can be confusing at first, so we will spend some time to understand how it operates. Is is worth noting that, while the borrow checker is somewhat unique to the Rust programming language, similar concepts have been employed in other languages through external tools called static analyzers, which scan through the code and try to uncover common errors related to resource usage (among other things).
Recap
In this section, we learned the basic of resource ownership and the resource lifecycle. We saw why systems programming languages might give the programmer the tools to explicitly manage hardware resources and what the downsides to this are (mental load, higher probability of bugs). We then saw that there are ways to mitigate these downsides by analyzing resource lifetimes in the code, for example using the borrow checker in Rust.
In the next chapter, we will look at the last major feature of Rust, namely its approach to polymorphism and why Rust is not considered an object-oriented language.
Closing exercises
🔤 Exercise 2.5.1
Using the programming language of your choice, write a program that demonstrates resource exhaustion for each of the following resources:
- Memory
- Files / file descriptors
- Network connections / sockets
Answer these questions first before writing your program, as they will help you conceptualize what your program has to do:
- How can I allocate the specific resource using your my chosen programming language?
- How many "elements" of each resource do I expect I should be able to allocate from a single program?
- For files: Is there a difference between opening the same file multiple times, or opening multiple different files? If I expect that I would need to open multiple different files simultaneously, how would I get access to a large number of different files on my file system?
- For network connections: Do I need multiple programs to establish a network connection (e.g. client and server) or can I do it within a single program?
Then run each of your programs to verify that you can exhaust the given resource. How does your programming language and operating system handle the resource exhaustion?