Programmiersprache: Rust für Neugierige

Rust ist stark im Aufwind. Rust-Neulinge lernen in diesem Artikel die Gründe dafür sowie die einzigartigen Fähigkeiten der Programmiersprache kennen.

In Pocket speichern vorlesen Druckansicht 299 Kommentare lesen

(Bild: Dilok Klaisataporn/Shutterstock.com)

Lesezeit: 14 Min.
Von
  • Stefan Baumgartner
Inhaltsverzeichnis

Die Programmiersprache Rust wurde in diesem Jahr erneut zur beliebtesten in der jährlichen Stack-Overflow-Entwicklerumfrage gewählt – zum achten Mal in Folge. Doch nicht nur Entwicklerinnen und Entwickler schätzen die Programmiersprache, sondern auch die Industrie schenkt ihr Beachtung. Große Unternehmen wie Microsoft, AWS und Google setzen in vielen Bereichen auf ihre einzigartigen Eigenschaften. Was macht diese Sprache so beliebt?

Rust wurde Anfang der 2010er Jahre von Graydon Hoare bei Mozilla im Rahmen der experimentellen Browser-Plattform Servo entwickelt. Das grundlegende Ziel war es, das Erstellen neuer Browserfunktionen zu ermöglichen und im gleichen Zug die Probleme der Speicherverwaltung und Speicherzuweisung von C und C++ zu beseitigen. Ähnlich wie die Programmiersprache Go sollte Rust performante Software ermöglichen und obendrein sollte die Entwicklung Spaß machen.

Dabei weist Rust Einflüsse aus vielen anderen Programmiersprachen auf. Die Syntax ähnelt stark C, aber es sind auch Spuren funktionaler Programmiersprachen vorhanden, insbesondere OCaml, worin die erste Version des Rust-Compilers geschrieben wurde.

pub fn score(word: &str) -> u64 {
    let mut score = 0;
    for ch in word.to_lowercase().chars() {
        score = score + match ch {
            'a' | 'e' | 'i' | 'o' | 'u' | 'l' | 'n' | 'r' | 's' | 't' => 1,
            'd' | 'g' => 2,
            'b' | 'c' | 'm' | 'p' => 3,
            'f' | 'h' | 'v' | 'w' | 'y' => 4,
            'k' => 5,
            'j' | 'x' => 8,
            'q' | 'z' => 10,
            _ => 0
        }
    }
    score
}

Listing 1: Berechnung eines Scrabble-Scores

Listing 1 zeigt eine Rust-Funktion, die die Punkte für ein Wort in einem Scrabble-Spiel berechnet. Interessanterweise ist dieser Codeausschnitt genauso lesbar wie entsprechender Python-Code, enthält aber auch Bequemlichkeitsfunktionen wie Ausdrücke.

Diese ermöglichen es, einen match-Ausdruck direkt neben der Addition von zwei Zahlen zu haben. Der match-Ausdruck selbst ermöglicht Pattern Matching über eine Reihe von gültigen Werten. In diesem Fall geht das Pattern Matching alle möglichen Werte des char-Werteraums durch. Das Schlüsselwort match verlangt von der Anwendung, alle möglichen Werte zu behandeln. Da es mehr Zeichen als die des Alphabets gibt, kann ein Standardfall mit dem Unterstrichzeichen alle verbleibenden Werte abfangen.

Neben der neuen und modernen Syntax besticht Rust durch weitere Eigenschaften. Das Projekt "Rust in Linux" listet einige Funktionen auf, die die Sprache zu einer wertvollen Ergänzung des Linux-Kernels machen:

  • Rust hat kein undefiniertes Verhalten, bietet Speichersicherheit und verhindert Datenkonflikte.
  • Sein striktes Typsystem hilft, logische Fehler zu reduzieren.
  • Wenn man nicht speichersicheren Code braucht, wird das mit dem unsafe-Schlüsselwort gekennzeichnet. Das hilft bei der Entwicklung komplexer Datenstrukturen, ist aber im täglichen Gebrauch kaum nötig.
  • Rust besitzt eine umfangreiche, eigenständige Standardbibliothek. Da diese optional ist, eignet sich Rust gut für eingebettete Systeme.
  • Die integrierte, sofort einsatzbereite Tool-Unterstützung erleichtert das Erstellen, Kompilieren und Strukturieren von Projekten.

Dank dieser Eigenschaften hat Rust das Interesse großer Unternehmen auf sich gezogen. Teile der AWS-Infrastruktur wie Lambda oder Fargate sind in Rust geschrieben. Mit den neuesten Versionen von Windows wurde Rust ebenfalls ausgeliefert und Teile von Microsoft Azure sind in Rust geschrieben. Das Android-Projekt verfügt bereits über rund 1,5 Millionen Zeilen Rust-Code, und das Google-Chrome-Projekt arbeitet daran, Rust ebenfalls an vorderster Stelle einzusetzen.

Um zu verstehen, warum diese Big-Tech-Unternehmen Rust verwenden, hilft ein genauerer Blick auf drei wesentliche Funktionen:

  • Speichersicherheit ohne Garbage Collection
  • Abstraktion ohne Overhead
  • Furchtlose Nebenläufigkeit

Jahrzehntelang waren die am häufigsten verwendeten Programmiersprachen in zwei Lager unterteilt: Zum einen war es möglich, Software in Programmiersprachen mit automatischem Speichermanagement zu schreiben. Dazu gehörten Skriptsprachen wie Ruby, Python oder JavaScript, Virtual-Machine-basierte Sprachen wie C# oder Java und einige nativ kompilierte Sprachen wie Go. Zum anderen konnten Entwickler Speicher selbst verwalten, so wie es in C oder C++ möglich ist. Das erlaubte die Entwicklung sehr performanter Software, da keine Laufzeitumgebung erforderlich war, um über Garbage Collection den Speicher zu verwalten.

Rust verfolgt jedoch eine andere Strategie: Es setzt auf eine Speicherzuweisung zur Kompilierzeit, die auf einem strengen Regelwerk basiert, dem Entwicklerinnen und Entwickler folgen müssen. Das erfordert etwas Lernaufwand, führt aber zu hundertprozentig speichersicheren Anwendungen. Und Speichersicherheit ist entscheidend: Die Verwendung von Speicher nach dessen Freigabe, doppelte Speicherfreigaben oder Pufferüberläufe und -überschreibungen führen zu Sicherheitslücken, die sich leicht ausnutzen lassen.

Microsoft und Google haben in Windows und im Chrome-Projekt jeweils 70 Prozent aller schweren Sicherheitsprobleme als Speicherprobleme entlarvt. Eine Programmiersprache, die ohne einen Performance verschlingenden Laufzeit-Garbage-Collector vollständig speichersicheren Code erstellt, kann viele Probleme beseitigen. Das bestätigt auch das Android-Projekt: Im Dezember 2022 waren bereits 20 Prozent des nativen Codes von Android in Rust geschrieben, und bis heute sind im Rust-Code von Android keine Speichersicherheitsprobleme aufgetreten.

Rust erreicht die Speichersicherheit durch sein "Ownership and Borrowing"-System. Dessen grundlegendes Prinzip besagt, dass es nur einen Besitzer (Owner) von Daten geben kann. Wenn ein Wert einer Variable zugewiesen wird, wird diese Variable zum Besitzer. Wenn der Besitzer den Gültigkeitsbereich verlässt, wird der Speicher freigegeben. Die Eigentümerschaft (Ownership) lässt sich jedoch übertragen.

Das Beispiel in Listing 2 zeigt ein typisches Szenario, das in einer Programmiersprache wie Java oder JavaScript kein Problem darstellen würde, aber in Rust zu Kompilierfehlern führt. In dem Moment, in dem der Variablen other_numbers der Wert von numbers zugewiesen wird, hat die Variable numbers keinen Wert mehr. other_numbers besitzt die Daten, und der Compiler gestattet Entwicklern nicht, das ursprüngliche Binding weiter zu verwenden.

fn main() {
    let numbers = vec![1, 2, 3, 4];
    let other_numbers = numbers;
    println!("{:?}", numbers);
}

Listing 2: Ein Wert, auf den zwei Variablen zeigen. Dieses Beispiel wird nicht kompiliert.

Allerdings bietet der Rust-Compiler genaue Informationen darüber, was geschehen ist, und bietet in der Regel auch Lösungsvorschläge, wie die Abbildung zeigt.

Der Rust Borrow Checker lässt nicht zu, dass der gleiche Wert an zwei Variablen hängt. Das Ownership-Modell braucht immer einen eindeutigen Besitzer. In diesem Beispiel geht der Besitz der Werte von numbers an other_numbers.

In der Programmierung ist es oft notwendig, mehrere Zugriffe auf die gleichen Daten zu gestatten, weshalb es einschränkend sein kann, immer nur einen Besitzer zu haben. Daher erlaubt Rust das Ausleihen (Borrowing) von Daten durch das Erstellen von Referenzen, die auf die gleichen Daten zeigen. Das ursprüngliche Binding behält dabei weiterhin die Eigentümerschaft. Listing 3 zeigt, wie man Referenzen erstellt.

fn main() {
    let numbers = vec![1, 2, 3, 4];
    let other_numbers = &numbers;
    println!("{:?}", numbers);
}

Listing 3: Ein funktionierendes Beispiel mit Referenz

Das kaufmännische Und (&) gehört zum Typ. Es zeigt exakt, was die Funktion erwartet, beispielsweise wenn sie in Funktionssignaturen Referenzen deklariert. Dann überprüft der Rust-Compiler, ob die Variablen, die im Besitz der Daten sind, lange genug leben und nicht außerhalb des Gültigkeitsbereichs verschwinden, bevor die Referenzen dies tun. Wird ein Verweis verwendet, nachdem der Besitzer verworfen wurde, lässt Rust den Code nicht kompilieren.

Das Verändern von Daten erfordert das Deklarieren einer Bindung als veränderlich (mutable, kurz mut). Außerdem sind veränderliche Referenzen zu erstellen, wenn andere Teile des Codes die Daten des Besitzers ändern sollen, wie in Listing 4 gezeigt. Es können mehrere geteilte Referenzen vorhanden sein, aber nur eine veränderliche Referenz. Der Grund liegt darin, dass bei der Änderung von Daten durch die veränderliche Referenz eine neue Allokation entstehen könnte, worduch die geteilten Referenzen auf einen falschen Wertebereich verweisen würden. Rust stellt sicher, dass kein Zeiger auf Daten zeigt, die ungültig werden könnten.

fn append(vec: &mut Vec<u32>) {
    vec.push(5);
}
fn main() {
    let mut numbers = vec![1, 2, 3, 4];
    append(&mut numbers);
    println!("{:?}", numbers);
}

Listing 4: Veränderlicher Zugriff auf numbers zur Änderung der Daten

Der wichtigste Aspekt von Ownership und Borrowing ist, dass im Code deutlich wird, was geschieht. Die Typen zeigen, was Entwickler erwarten können, und der Compiler versteht, wie der Speicher verwaltet werden muss. Unabhängig davon, wie ein Rust-Programm strukturiert ist, bieten die Ownership- und Borrowing-Regeln viele Informationen über die Feinheiten von Zuweisungen und Variablen.