zur Übersicht

Rust

Lesedauer ca. 5 Minuten
08.11.2023

In der Softwarebranche herrscht nie Stillstand. Ständig entstehen neue Technologien, Frameworks und Programmiersprachen. Damit wir im Unternehmen technologisch immer up to date sind, befassen sich unsere Xperten regelmäßig mit aktuellen Themen. Dieses Mal haben wir die Programmiersprache Rust genauer unter die Lupe genommen. Dabei wollten wir wissen, was es mit der systemnahen Low-Level-Sprache auf sich hat, die C/C++ sehr ähnlich ist.

Was ist Rust?

Die Entwicklung von Rust wurde 2009 durch Mozilla Research unterstützt. Motivation war es, eine Sprache zu entwerfen, die es Entwickelnden ermöglicht, kleinen, schnellen Code ohne Speicherfehler zu schreiben. Schließlich wurde die Sprache in Anlehnung an die Bezeichnung der Rostpilze Rust genannt. Diese Pilzgruppe ist besonders dafür bekannt, außerordentlich robust und überlebensfähig zu sein. Eigenschaften, die auch auf Rust zutreffen. Seit einigen Jahren nun ist die Weiterentwicklung der Sprache ein Open-Source-Projekt. Für die Installation wird der Toolchain Installer Rustup benötigt, mit dem auch der Rust Compiler und das Build Tool Cargo aktualisiert werden können.

Trotz des hohen Abstraktionsgrads zur eigentlichen Hardware ist Rust dennoch eine systemnahe Programmiersprache. Sie kommt ohne den Overhead eines Garbage-Collection-Mechanismus aus. Die hohe Speichersicherheit ist sicherlich der größte Unterschied zu den klassischen Systemprogrammiersprachen wie C oder C++. Rust versucht mit unterschiedlichen Mitteln Fehler wie falschen Speicherzugriff, Pufferüberläufe oder Race Conditions zu verhindern. Statische Codeanalyse durch den Compiler spielt dabei eine wichtige Rolle. Hierzu zählen auch Konzepte wie standardmäßige Unveränderlichkeit bei Variablen sowie exklusiver Eigentümerschaft von Speicherstellen.

Im Vergleich zu herkömmlichen Sprachen sind diese in Rust grundlegenden Konzepte und Techniken neu und vielen Entwickelnden nicht so vertraut. Anhand von zwei Beispielen verdeutlichen wir in den folgenden Abschnitten die einige der Schlüsselaspekte von Rust.

Beispiele

Im Listing 1 sehen wir ein Beispiel-Programm für die Verwendung von mutable und immutable Variablen.

Grundsätzlich werden Variablen folgendermaßen deklariert:

  • Es genügt das Schlüsselwort let.
  • Optional kann auch die Angabe des Typs und die Zuweisung eines Wertes erfolgen.
  • Handelt es sich um eine immutable Variable (nur Lesezugriff), was standardmäßig der Fall ist, kann nur maximal einmal ein Wert zugewiesen werden.
  • Für die Deklaration einer mutable Variablen (Lese- und Schreibzugriff) muss das Schlüsselwort mut nach let verwendet werden.
  • Die Gültigkeit einer Variablen ist der umgebende Anweisungsblock, begrenzt durch geschweifte Klammern.

LISTING 1 VARIABLEN UND IHRE VERÄNDERLICHKEIT:

fn main() {
    let x : i32;
    x = 3;
    // x = 4; -> Compiler Error
    println!("{}", x); // -> 3

    let mut y = 5;
    y += 5;
    println!("{}", y); // -> 10
    {
        let y = y * 2; // -> 20
        println!("{}", y);
    }
    y += 1;
    println!("{}", y); // -> 11
}

Listing 2 zeigt die Konzepte Ownership (Eigentum), Reference (Referenz) und Borrowing (Ausleihe). Der Speicher ist hier explizit zuzuweisen und freizugeben. Dabei müssen bestimmte Regeln, die das Eigentum des Speicherbereichs verwalten, eingehalten werden. Diese werden bereits vom Compiler überprüft. Die Regeln sind:

  • Jeder Wert in Rust hat einen Eigentümer.
  • Es kann immer nur einen Eigentümer geben.
  • Wenn der Eigentümer den Gültigkeitsbereich verlässt, wird der Wert gelöscht.

Darüber hinaus gibt es auch Regeln für Referenzen:

  • Zu jedem beliebigen Zeitpunkt kann entweder eine veränderbare Referenz oder eine beliebige Anzahl unveränderbarer Referenzen existieren.
  • Referenzen müssen immer gültig sein.

Zur Veranschaulichung verwenden wir den Datentyp String. Es handelt sich dabei um einen nicht-skalaren Datentyp, der die auf dem Heap (neben Stack ein weiterer Bereich des Speichers) zugewiesenen Daten verwaltet. Eine veränderbare Variable wird definiert und der benötigte Speicher angefordert. Bei der Übergabe an eine Funktion wandert der Wert in die Funktion. Die Variable verlässt ihren Gültigkeitsbereich und ist für einen Zugriff nicht mehr verfügbar.

Deshalb wird in diesem Beispiel mit Clone eine Kopie der Variablen erzeugt und übergeben. Es ist auch möglich, eine Referenz als Funktionsparameter zu verwenden. Hierbei wird ein Verweis auf ein Objekt als Parameter genutzt, anstatt die Eigentümerschaft des Wertes zu übergeben. Eine Referenz ist insofern ein Zeiger, da es sich um eine Adresse handelt, die auf den Ort der Daten zeigt. Referenzen werden mit einem &-Zeichen generiert. Man spricht dann von einem Borrowing (Ausleihe). In unserem Anwendungsfall wird eine veränderliche Referenz an die Funktion übergeben. Veränderliche Referenzen haben vor allem eine große Einschränkung: Während exakt nur ein schreibender Eigentümer vorhanden sein darf, können beliebig viele einen Lesezugriff erhalten. Dafür jedoch darf der Wert nach der Zuweisung nicht mehr verändert werden.

LISTING 2: OWNERSHIP:

fn main() {

    let mut string_original = String::from("Original");
    ownership_with_drop_after(string_original.clone());
    println!("{string_original}"); // -> Original

    string_original = ownership_with_drop_after(string_original);
    println!("{string_original}"); // -> Original : Geändert

    println!("########"); 

    let mut string_reference_pointer = String::from("Pointer");
    change_with_pointer(&mut string_reference_pointer);
    println!("{string_reference_pointer}"); // -> Pointer Reference
}

fn ownership_with_drop_after (mut s : String) -> String {
    s.push_str(" : Geändert");
}

fn change_with_pointer(s : &mut String) {
    s.push_str(" Reference");
}

Fazit

Im Vergleich zu anderen Programmiersprachen bietet Rust insbesondere Vorteile, wenn es um Stabilität, Sicherheit und Performance geht. Der Einstieg in Rust ist allerdings durch die grundlegenden Konzepte der Sprache und den daraus resultierenden Architekturen nicht gerade einfach. Daher ist es empfehlenswert, zu Beginn nicht vollständig auf Rust umzusteigen oder es voreilig in großen Projekten zu implementieren.

Vor allem hybride Ansätze erweisen sich als sinnvoll, bei denen Rust-Komponenten als Microservices oder Bibliotheken entwickelt und integriert werden. Eine weitere vielversprechende Option ist die Erzeugung von WebAssembly-Bytecode in Rust, was zukünftig in der Praxis sehr nützlich sein kann. Auf diese Weise können rechenintensive oder Low-Level-Aufgaben effizient in Rust bewältigt werden.

Rust findet auch Anwendung im Bereich Embedded-Systeme. Durch das Ownership-Konzept gewährleistet es eine große Speichersicherheit. Da Rust ohne Laufzeitumgebung und Garbage Collector auskommt, weißt es auch einen vergleichsweise geringen Ressourcenverbrauch auf.