Rust ist eine Multiparadigmen-Systemprogrammiersprache, die von der Open-Source-Community entwickelt wird. Sie wird mit dem Ziel entwickelt, jeden dazu zu befähigen, zuverlässige und effiziente Software zu entwickeln. Zuverlässigkeit bezieht sich dabei insbesondere auf die Vermeidung von Programmfehlern, die zu Speicherzugriffsfehlern oder Pufferüberläufen und damit unter Umständen auch zu Sicherheitslücken führen, vor allem auch in nebenläufigen Prozessen. Der Wertebereich von Variablen wird dabei mittels eines Typsystems eingeschränkt und die angestrebte Typsicherheit wurde formal bewiesen.

Rust vereint Ansätze aus verschiedenen Programmierparadigmen, unter anderem aus der funktionalen, der objektorientierten und der nebenläufigen Programmierung und erlaubt so ein hohes Abstraktionsniveau. Beispielsweise gibt es in Rust algebraische Datentypen, Pattern Matching, Traits (ähnlich den Typklassen in Haskell), Closures sowie Unterstützung für RAII. Die Sprache wurde so entworfen, dass die Kosten der Abstraktionen zur Laufzeit so gering wie möglich bleiben können (zero-cost abstractions), um eine mit C++ vergleichbare Performanz zu erreichen.

Rust wurde ursprünglich bei Mozilla entwickelt. Seit 2021 besteht allerdings die Rust Foundation, eine gemeinnützige Stiftung, die die Weiterentwicklung der Sprache betreut und das Eigentum an den Marken und Logos von Rust hat. Zu den Sponsoren der Stiftung und der Entwickler gehören große Konzerne wie Google, Amazon AWS, Meta und Microsoft.

Geschichte Die Sprache entstand aus einem persönlichen Projekt des Mozilla-Mitarbeiters Graydon Hoare. Sein Fokus lag darauf, eine Programmiersprache mit sicherer Speicherverwaltung zu entwerfen, die schnellen und kompakten Code generiert. Frühe Versionen von Rust unterschieden sich selbst von der Version 1.0 wesentlich, so gab es automatisch verwaltete Zeiger mittels ~T ähnlich wie in Swift, ein Effektsystem, Green Threads, prototypen-ähnliche Objektorientierung, und heutige Features wie async, const und der Fehlerbehandlungsoperator ? fehlten.

Mozilla begann das Sponsoring des Projekts im Jahr 2009. Es wurde 2010 zum ersten Mal angekündigt. Im selben Jahr begann der Wechsel von einem (noch in OCaml geschriebenen) ersten Compiler zu einem neuen Compiler, der selbst in Rust geschrieben ist. Dieser rustc genannte Compiler verwendet LLVM als Back-End und kann sich seit 2011 erfolgreich selbst übersetzen. Eine erste stabile Version von Compiler und Standardbibliothek, Rust 1.0, wurde am 15. Mai 2015 veröffentlicht.Announcing Rust 1.0

Im August 2020 entließ Mozilla im Rahmen einer Unternehmensumstrukturierung unter anderem große Teile des Rust-Teams. Das Servo-Team wurde komplett aufgelöst. Nach Angaben der Vorstandsvorsitzenden Mitchell Baker wurde dies durch die Folgen der COVID-19-Pandemie notwendig. Das Ereignis führte zu Bedenken über die Zukunft von Rust.

Daher gründete das Rust-Kernteam eine Stiftung, um das Eigentum an allen Markenzeichen und Domainnamen zu übernehmen wie auch die finanzielle Verantwortung für deren Kosten. Am 8. Februar 2021 gaben die Unternehmen AWS, Huawei, Google, Microsoft und Mozilla sowie fünf Mitglieder aus dem Rust-Team die Gründung der Rust Foundation bekannt.

Sprachdesign Syntax, Sprachkonstrukte Syntaktisch ist die Sprache an C angelehnt. Sie besitzt auch Einflüsse aus der ML-Sprachfamilie. Mehrere aufeinanderfolgende Anweisungen werden durch ein Semikolon getrennt, Blöcke stehen in geschweiften Klammern. In vielen Details weicht die Syntax allerdings davon ab. So werden beispielsweise keine Klammern um die Bedingungen von if- und while-Statements benötigt, stattdessen aber geschweifte Klammern um den dahinterstehenden Block erzwungen, auch wenn dort nur eine Anweisung steht. Das Schlüsselwort for leitet in Rust immer eine Foreach-Schleife ein, die das Iterieren über beliebige (auch benutzerdefinierte) Container und Objekte ermöglicht. Anstelle des switch-Statements gibt es die wesentlich mächtigeren match-Ausdrücke, die nicht nur mit Zahlen und Zeichenketten umgehen können, sondern Pattern Matching auf beliebig verschachtelten Strukturen durchführen können.

Die Styleguides empfehlen die Namenskonventionen snake_case für Variablen, SCREAMING_SNAKE_CASE für Konstanten und PascalCase für Datentypen, mit Ausnahme der Primitiven. Typparameter von generischen Typen und Funktionen stehen in spitzen Klammern. Zwei Doppelpunkte drücken wie in C++ aus, dass der Bezeichner auf der rechten Seite aus dem Namensraum des Bezeichners auf der linken Seite entstammen soll. Die Bindung eines Bezeichners x an einen Wert wird

let x: Typ = Wert;

geschrieben, wobei die Angabe des Typs auch entfallen darf, wenn der Typ abgeleitet werden kann. Diese Syntax entstammt der ML-Familie an Programmiersprachen. Eine Variablen-Deklaration für die Variable v hat die Form:

let mut v: Typ = Initialwert;

oder allgemeiner:

let mut v: Typ; v = Wert;

Der Compiler überprüft mittels Datenflussanalyse, ob alle Bindungen und Variablen vor dem Auslesen initialisiert wurden. Verwendung von uninitialisiertem Speicher ist nur über einen speziellen Befehl möglich, welcher zwingend mit unsafe markiert werden muss.

Typsystem Benutzerdefinierte Datentypen können entweder als struct (Struktur wie in C, Produkttypen) oder als enum (tagged-Union, Summentypen) definiert werden. Dabei ist enum vielseitiger als das gleichnamige Schlüsselwort in C und C++: In Rust können die Varianten des enum auch innere Werte (wie Strukturen oder Tupel) enthalten.

struct Person { age: u8, first_name: String, last_name: String, }

impl Person { // Erzeuge ein neues Person-Objekt. // Diese Funktion hat keine besonderen Eigenschaften. // "new" ist nur eine Konvention. fn new(first_name: String, last_name: String) -> Self { Self { first_name: first_name, last_name: last_name, age: 0,

// Einfaches Enum ohne Werte in den Varianten. // In dieser Form identisch zu C/C++. enum Weekday { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday, }

// Vereinfachter mathematischer Ausdruck. enum Expression { Add { left: u64, right: u64, }, Multiply { left: u64, right: u64, }, Negate(u64), Literal(u64), Invalid, }Für beide Arten von Datentypen kann man mittels des impl-Schlüsselwortes Methoden definieren. Die sonst für objektorientierte Programmierung übliche Vererbung gibt es in Rust allerdings nicht; Polymorphie wird stattdessen durch Traits und generische Programmierung ermöglicht. Ein Trait definiert eine Menge von Funktionen und Methoden, die dann jeweils zusammen von Datentypen implementiert werden und bei Typparametern als Einschränkung für die erlaubten Typen dienen können. Dies wird auch für Operatoren verwendet, sodass beispielsweise der Operator + mit jedem Datentyp verwendet werden kann, der den Trait Add implementiert.Dokumentation des „Add“-Traits Alle Funktions-, Methoden- und Operatoraufrufe werden dabei statisch gebunden, wodurch dem Compiler einige Optimierungen ermöglicht werden. Allerdings kann über sogenannte Trait-Objects auch mittels dynamischer Bindung auf Trait-Methoden zugegriffen werden. Es ist erlaubt, sowohl existierende Traits aus fremdem Code für benutzerdefinierte Typen als auch neue, benutzerdefinierte Traits für existierende Typen zu implementieren.

Zeiger, Speicherverwaltung Rust kennt neben den sogenannten Raw-Pointern, die Zeigern in C entsprechen und nur in explizit als unsafe markiertem Code dereferenziert werden dürfen, auch noch Referenzen. Diese zeigen immer auf gültigen Speicher und dürfen niemals den Wert null annehmen. Es werden zwei verschiedene Arten von Referenzen unterschieden: gemeinsame Referenzen (shared references), eingeleitet durch &, und veränderbare Referenzen (mutable references), eingeleitet durch &mut. Der Rust-Compiler garantiert statisch, dass keine Referenz das von ihr referenzierte Objekt „überlebt“, dass das referenzierte Objekt nicht verändert wird, während eine Referenz (egal ob gemeinsam oder veränderbar) existiert, und dass eine veränderbare Referenz – wenn eine solche existiert – stets die einzige Referenz auf ein Objekt ist, sodass Veränderungen am Objekt vorgenommen werden können, ohne dabei Referenzen zum selben Objekt an anderen Stellen im Code oder sogar in anderen Threads ungültig zu machen. Um ein eigentlich unveränderliches Objekt innen veränderlich zu machen, gibt es den Hilfstyp Cell.

Das Erstellen von Referenzen wird auch als Borrowing (Ausleihen) bezeichnet und bildet zusammen mit dem Konzept der Ownership (Besitz) die Grundlage für die sichere Speicherverwaltung ohne Garbage Collection. Ownership bedeutet hier, dass jedes Objekt im Speicher im Besitz derjenigen Variable ist, der es bei der Erstellung zugewiesen wird. Am Ende der Lebensdauer dieser Variable wird der Speicher automatisch freigegeben. Für die meisten komplexeren Datentypen verwendet Rust die sogenannte Move-Semantik, sodass bei Zuweisung eines Objektes zu einer anderen Variablen das Objekt „verschoben“ wird, und ein Zugriff auf die alte Variable daraufhin nicht mehr möglich ist. Auch beim Übergeben von Objekten als Wertparameter an Funktionen (call by value) werden solche Objekte in die Funktion „hineingeschoben“ und sind von außen nicht mehr zugreifbar, wenn sie nicht wieder von der Funktion zurückgegeben werden. Primitive Datentypen im engeren Sinne verwenden keine Move-Semantik, sondern die sonst übliche Copy-Semantik (Zuweisungen erstellen eine Kopie). Dies gilt nicht für folgende Datentypen, die in Rust als primitiv gelten: veränderbare Referenzen (mutable references), manche Closures, str und manche Arrays und manche Tupel. Bei benutzerdefinierten Datentypen kann mittels des Copy-Traits selbst entschieden werden, ob Move- oder Copy-Semantik verwendet werden soll.

Zur dynamischen Speicherverwaltung stehen die Smart-Pointer-Typen Box, Rc und der Hilfstyp RefCell zur Verfügung, welche einerseits dem System Ownership-Borrowing-Lifetime-Move unterworfen sind, dieses System andererseits auf die Laufzeitdynamik übertragen. Der Typ Box<T> beschreibt einen besitzenden Zeiger auf einen Wert vom Typ T im dynamisch allozierten Speicher. Der Typ Rc<T> beschreibt über Referenzzählung einen gemeinschaftlichen Besitz auf einen unveränderlichen Wert. Ein unveränderlicher Wert kann jedoch veränderliche innere Daten vom Typ RefCell<T> besitzen, wobei jeweils ein Referenzzähler für einen Zugriff analog zu & und &mut das sichere dynamische Borrowing durch Prüfung zur Laufzeit ermöglicht.

Mit Rc allein ist keine Dynamik zyklischer Datenstrukturen möglich. Hierzu kann wieder RefCell herangezogen werden, wobei die Zyklen manuell aufgebrochen werden müssen, damit es nicht zu einem Speicherleck kommt. Als alternatives Konzept ist der Zeigertyp Weak<T> vorhanden, bei dem die Zeiger analog zu Rc sind, jedoch keinen Besitz am Wert haben. Zur Umsetzung von Weak ist in Rc ein zweiter Referenzzähler implementiert. Ein Zeiger vom Typ Weak verhindert zwar nicht den Destruktor-Aufruf beim Verschwinden aller Zeiger vom Typ Rc, die Deallokation des brach liegenden Knotens geschieht dann allerdings erst beim Verschwinden des letzten Zeigers vom Typ Weak. Die Destruktor-Aufrufe sorgen dabei für ein automatisches Aufbrechen der Zyklen.

Für die nebenläufige Programmierung ist ein Austausch von Rc gegen Arc, und RefCell sowie Cell gegen die analogen Konzepte Mutex oder RwLock notwendig.

Nullzeiger kommen in Rust nicht vor. Abwesenheit eines Werts wird stattdessen durch den Typ Option<T> modelliert, welcher es als enum ermöglicht, zwischen Some(T) (Vorhandensein) und None (Abwesenheit) zu unterscheiden. Die Verwendung von Optionen ist nicht auf Zeiger beschränkt. Möchte man z. B. einen großen Teil einer struct zunächst uninitialisiert lassen, dann kann man diesen als innere struct hinter einer Option verbergen und mit None initialisieren.

Fehlerbehandlung