Ferris Talk #11: Memory Management - Memory management in Rust with ownership .

Rust Coding

6 Views

        

Once you have understood the ownership model and the borrow checker in Rust, the gates are wide open for memory and runtime efficient programming.

The Rust programming language brings many concepts into the mainstream that are otherwise only found in niche languages ​​or academic publications. This is not just about subtleties, but about fundamental properties of the language. One of these concepts is so unique that it represents completely new territory for many: memory security without a garbage collector.

All software has to manage memory in some way. Two models have prevailed: on the one hand, automatic memory management-of-all-its-users/">management by a garbage collector, and on the other, independent management by the developer himself.

To put it simply, the automatic memory management system uses a runtime environment to keep track of the number of users of a memory unit. If Runtime notices that there are no more variables that point to the memory area, the proverbial garbage man comes and releases the reserved memory again.

With Java, garbage collection became popular in the 1990s, and today this type of management is probably the most common. The biggest advantage is that memory leaks are less likely to happen, resulting in more secure software. The runtime environment has a disadvantageous effect on speed-critical systems, since it has to constantly check memory movements and pointers to memory areas in the background.

In this column, the two Rust experts Rainer Stropek and Stefan Baumgartner would like to take turns reporting regularly on innovations and background information in the Rust area. It helps teams already using Rust stay current. Beginners get deeper insights into how Rust works through the Ferris Talks.

  • Ferris Talk #1: Iterators in Rust
  • Ferris Talk #2: Abstractions Without Overhead – Traits in Rust
  • Ferris Talk #3: New Rust 2021 Edition is here – Closures, the Cinderella feature
  • Ferris Talk #4: Asynchronous Programming in Rust
  • Ferris Talk #5: Tokyo as an asynchronous runtime environment is an almost all-rounder
  • Ferris Talk #6: A new trick for the format strings in Rust
  • Ferris Talk #7: From Behemoth to Gold Rose – a little Rust refactoring story
  • Ferris Talk #8: Wasm loves Rust – WebAssembly and Rust beyond the browser
  • Ferris Talk #9: The Builder Pattern and Other Typestate Adventures
  • Ferris Talk #10: Constant Fun with Rust
  • Ferris Talk #11: Memory Management – Memory management in Rust with ownership

The other type places the responsibility of memory management on the programmers. You have to use commands to explicitly request memory areas and release them elsewhere. C and C++ are the most popular representatives of this type of memory management. Here’s an example of how to do it in C using malloc and free Request, fill and then release memory for three consecutive integers:

#include <stdio.h>
#include <stdlib.h>

int main() {
    int* ptr;
    int n = 3;
    // Speicher anlegen
    ptr = (int*) malloc(n * sizeof(int));

    // Befuellen
    for(int i = 0; i < n; i++) {
        ptr[i] = i;
    }
  
    for(int i = 0; i < n; i++) {
        printf("%d ", ptr[i]);
    }

    printf("\n");

    // Freigeben
    free(ptr);
}

Listing 1: Dynamic memory allocation and filling of three integers in C

So far, so awkward. Manual storage management is clearly associated with considerable effort. However, it has the advantage that only the memory that is actually used is allocated. In this way, particularly memory and runtime efficient programs can be written.

However, the disadvantages outweigh the disadvantages and have even been given their own names in the industry:

  • memory leaks. The memory that you once requested must be actively released again later. Otherwise, leaks and an overfilled storage can quickly occur.
  • What if you free memory too soon? A use-after-free error means that you are trying to access memory that has already been freed. This leads either to corrupted data or to program crashes.
  • On the other hand, a double-free error can free memory more than once.
  • With buffer overreads and overwrites you literally overshoot the mark and read and write in areas that don’t even belong to you.

These problems are not only extremely annoying and can lead to program crashes, but are also security-critical. The entries CWE-401, CWE-415 and CWE-416 of the Common Weakness Enumeration show which attack vectors can result from problems with manual memory management.

These problems are numerous. Both Microsoft and Google describe that 70 percent of security-critical bugs in Windows and Chrome can be traced back to memory security problems.

Rust’s memory model takes care of this category of problems. Instead of automated and therefore slow memory management by a runtime environment or manual, but highly complex and security-critical management by humans, Rust has a third type of memory management: the ownership model.

In the ownership model, the compiler takes on the necessary instructions to allocate memory and release it again, but requires developers to comply with a simple but profound set of rules:

  1. Every value in Rust is attached to a variable called the possessor.
  2. There is exactly one owner for each value.
  3. If the owning variable is no longer included in the execution context (scope), which is usually indicated by the curly brackets at the end of a block, the previously reserved memory is released.

Rust Meetup Linz – Florian Gilcher, “Drum check who binds himself forever – Ownership and Borrowing from a Systems Construction Point of View” (March 2021)

Memory is allocated when a variable is initialized. This concept is called RAII (resource acquisition is initialization) and is also known from C++. For simple programs this can look like the following listing:

fn main() {
    // Speicher fuer vec wird hier angelegt
    let vec = vec![0, 1, 2]; 

    println!("{:?}", vec);
} // vec wird hier freigegeben.

Listing 2: Dynamic memory allocation and filling for three integers in Rust using the vec macro

Attempting to assign the same value to another variable – which works fine in almost any other programming language – violates the most important rule of the ownership model: “There can only be one” (meaning the owner). Listing 3 illustrates the principle.

fn main() {
    let vec = vec![0, 1, 2]; // Speicher für vec wird hier angelegt
    let another_vec = vec; // Zuweisung
    println!("{:?}", vec); // vec ist nicht mehr Besitzer der Daten
}

Listing 3: Failed attempt to access data that already has another owner

The compiler does not translate the example. At the time when vec would be output on the command line is nice another_vec owner of the created data. The Rust compiler produces an error and a notification message that it’s accessing a value that “has already been moved”—that’s jargon for assignment to another variable.

The same principle applies when passing an allocated value to a function that requests ownership in the function signature, as shown in Listing 4:

fn main() {
    let vec = vec![0, 1, 2];
    print_vec(vec);
}

fn print_vec(vec: Vec<i32>) {
    println!("{:?}", vec);
}

Listing 4 The print_vec function requests ownership of the vector

When calling from print_vec Has main the possession of vec submitted to the function. At the end of print_vecfunction – and thus at the end of the execution context – the memory area of ​​​​all values ​​owned by print_vec are. Another call from print_vec With vec would cause a compiler error since the original data has already been destroyed.