We saw in the previous article the problems of manual memory management, but also the pitfalls of automatic memory management in languages like Java.
But what if there was a middle ground, what if we could find a way to get rid of rubbish collector pauses, but at the same time not have to manage memory manually? How do we do it?
Automatic management relies on the garbage collector, which runs at runtime, along with the program. Manual management falls to the programmer, who must do it at development time. If we discard the runtime collector and take away the programmer’s task during development, what are we left with? Easy: the compiler.
The third way: the compiler
The third way is to introduce (or rather, to make the compiler, which is another of the elements in play, responsible for the memory management options. That is to say, to make it responsible for identifying who requests memory, how it is used and when it stops being used in order to reclaim it.
The compiler, as an element of the development chain, has a broad view of all the elements that make up a program. It knows when memory is being requested and keeps track of the life of an object because it knows which symbol is being referenced, how and where. And of course, most importantly, when it stops being referenced.
This is what programming languages like Rust do, whose model is based on ownership with the following rules:
- Each value in Rust must have an “owner”.
- Only one “owner” can exist at a time. No more than one “owner” can exist.
- When the “owner” goes out of scope or visibility, the object is discarded and any memory it may contain is properly freed.
Rules are simple, but in practice it takes some getting used to and a high tolerance for frustration, as the compiler will interpose itself in the face of any slip-up that inadvertently leads to a violation of the above rules.
The system used by the compiler is called the borrow checker. It is basically the part of the compilation dedicated to check that the concept of owner is respected with respect to an object and that if an owner “borrows” the object, this borrowing is resolved when the scope changes and the concept of single owner is still maintained. Either because the recipient of the object takes responsibility for it or because he/she returns the ownership of the object.
If we look at the compiler complaints and the code, we see that the variable “s” has the string property “hello”. In line 3, we declare a variable called “t” that borrows (and does not return) the string “hello”.
Then, in line 4, we make “s” add a new string and complete the classic “hello world” sentence, but the compiler won’t let it: it’s an error and lets us know.
What has happened here?
The one-owner rule comes into play. The compiler has detected that “s” no longer owns the string property, which now resides in “t”, so it is illegal for it to make use of or attempt to modify the object it once owned, since it now doesn’t belong to it.
This is just the basics, but it is intended to give us an idea of how this “policing” of the rules by the compiler works.
By the way, where is the toll here? Of course, in very long compilation times compared to C language or even Java, e.g.
Rust goes a third way in terms of memory management and only sacrifices compile time, programmer frustration while developing, and a certain steep curve in getting used to memory management rules (we have illustrated the basics, but Rust has other notions that complement borrowing, such as lifetimes, etc.).
Despite the price paid (even so, the compiler improves timings with each new version), however, we will be almost absolutely certain that our program is free of memory errors (and also most of the race conditions) that could cause vulnerabilities.
Will Rust save the world? Time will tell, but the reception and adoption of the language is remarkable and will improve the outlook for memory error-based vulnerabilities, which is no small thing.
Featured image: Creativeart on Freepik.