2.4. 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 does 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
-
- Releasing the resource back to the source it was acquired from
Typically, the operating system acts as the administrator of hardware resources, though there are also systems without an operating system, such as embedded systems, where hardware resources are accessed directly by the programmer. 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 "Mom cleans up your room, 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.