Mozillas Programmiersprache: Rust bedient sich bei der Konkurrenz

Die statisch typisierte Programmiersprache Rust(öffnet im neuen Fenster) soll schnell und sicher sein. Seit dem Erscheinen der Version 1.0 gilt die Sprache als stabil, so dass sich nun auch externe Entwickler damit vertraut machen können. Dabei wird schnell klar, dass Rust viele Anleihen bei anderen Sprachen macht. Wir erklären mit Hilfe einfacher Beispiele im Folgenden grundlegende Konzepte von Rust.
Speicher und Zeiger
Rust speichert wie ihr großes Vorbild C Variablen fester Größe in einem Stack. Zeigerwerte legt sie hingegen im Heap des Hauptspeichers ab. Der folgende Code demonstriert den kontrollierten Umgang mit alloziertem Speicher unter Rust.
Geänderte Besitzverhältnisse
Wird der Code in der Datei moved_type.rs gespeichert und die Software anschließend über den Befehl rustc moved.rs in der Shell kompiliert, bricht der Compiler den Vorgang mit einer Fehlermeldung für die Zeile 8 ab:
moved_type.rs:8:3: 8:6 error: use of moved value: `vec`
In dem Beispiel verweist der im Hauptteil des Programms erzeugte Zeiger vec (Zeile 6) auf den Speicherbereich im Heap, der die Repräsentation des Vektors mit den Komponenten 1, 2 und 3 speichert. Zeile 7 übergibt den Zeiger an die Funktion print_length() und macht sie so zum exklusiven Besitzer. In der Konsequenz darf das Programm den Zeiger ab Zeile 7 nicht mehr verwenden, was zu der Fehlermeldung führt.
Soll der Zeiger dennoch weiterverwendet werden, kann er temporär an print_length() verliehen werden (Borrowing). Dazu wird in Zeile 7 eine Referenz (&vec) übergeben und analog die Funktionsdeklaration von print_length() in Zeile 1 modifiziert nach vec: &Vecᐸi32ᐳ . Soll print_length() zudem in der Lage sein, den Zeigerwert zu ändern, muss alternativ das Fragment vec: &mut Vecᐸi32ᐳ genutzt werden.
Rust ermöglicht es den Entwicklern aber nur, eine veränderbare Referenz zu übergeben. Um den Ressourcenverleih in jedem Aspekt der Programmierung richtig zu machen, bietet Rust neben dem Borrowing die Angabe von Lifetimes.
Beim Freigeben von alloziertem Speicher verfolgt die Programmiersprache einen eigenen Ansatz: Das Rust-Kompilat erledigt die Speicherfreigabe selbsttätig und verzichtet dabei auf einen Garbage Collector wie es ihn unter Java gibt. Auch die Funktion free() , wie sie C und C++ unterstützen, muss die Sprache nicht heranziehen, um den Speicherplatz programmgestützt freizugeben.
Ausdrucksstark
Wertet Rust einen Ausdruck aus, ist dessen Rückgabewert das Ergebnis. Doch auch alle Anweisungen haben einen Rückgabewert: ein leeres Tupel. Das Semikolon trennt, wie in vielen anderen Sprachen, Ausdrücke und Funktionen, um sie zu gruppieren.
Die Funktion aus dem nächsten Beispiel ermittelt das Maximum zweier ganzer Zahlen. Diese übernimmt sie in der Parameterliste nach dem Funktionsnamen in Zeile 1. Als Datentyp verlangt sie für die Parameter a und b je eine 32-Bit-Zahl. Der Rückgabewert rechts neben dem -ᐳ-Operator ist vom selben Typ.
Rückgabewerte
Zeile 2 verwendet das Schlüsselwort return ( ret in Version 0.3) und gibt a als Resultat zurück. Anderenfalls gibt Zeile 3 den Wert von b zurück. Die letzte Zeile des Funktionsrumpfes darf auf return verzichten.
Falls Zeile 3 irrtümlicherweise mit einem Semikolon abgeschlossen würde, wäre die Funktion ungültig, denn das würde den Ausdruck b in eine Anweisung verwandeln. Damit wäre der Typ des Rückgabewerts ein Tupel und nicht wie gefordert eine ganze Zahl.
Musterhaft und klassenlos
Rust bedient sich ausgiebig beim Repertoire anderer Sprachen. So verwendet sie wie die funktionalen Programmiersprachen Haskell und Standard ML Muster – und dies in zahlreichen Kontexten. Mit Hilfe des Musters (x,y,z) auf der linken Seite der Zuweisung let (x,y,z) = (1,2,3); deklariert die Anweisung die drei Variablen x, y und z auf einen Schlag. Diese auch als Destructuring Assignment bekannte Anweisung weist x den Wert 1, y die 2 und z die 3 zu.
Der Match-Ausdruck im nächsten Code verwendet in den Zeilen 2 und 3 jeweils ein Muster zur Fallunterscheidung links neben dem =ᐳ-Operator. Ergibt der Ausdruck x in Zeile 1 den Wert 0, speichert die Variable w die Summe aus y und z. In jedem anderen Fall speichert w das Produkt aus den Werten der beiden Variablen.
Mustererkennung
Klassenlos
Rust 1.0 lässt das Konzept der Klassen wieder fallen. Anstelle von Klassendefinitionen arbeitet Version 1.0 mit benutzerdefinierten, zusammengesetzten Datentypen, die sie bei Bedarf um Methoden ergänzt. Das Schlüsselwort struct leitet zu Beginn des folgenden Beispiels die Definition des benutzerdefinierten Datentyps Container ein. Eine Instanz von Container speichert dabei in Zeile 2 intern einen Vektor aus 64 Bit breiten Fließkommazahlen im Feld content .
Anwendungsdaten ohne Klassen (impl.rs)
Der Block nach dem Schlüsselwort impl ab Zeile 5 definiert die zum Datentyp Container passende Methode, so etwa print_content() (Zeilen 6 bis 8). Sie übernimmt dank der Variablen self eine Referenz auf sich selbst.
Die Zeile danach gibt die Anzahl der in content gespeicherten Fließkommazahlen mit Hilfe des Makros println über die Standardausgabe in der Shell aus. Dabei formatiert sie das Ergebnis auch gleich. Bevor das Makro dies allerdings tut, ersetzt Rust den Platzhalter {} in der Zeichenkette durch den Wert des Methoden-Aufrufs len für den Vektor aus der Komponente content .
Im Hauptteil des Programms erzeugt Zeile 12 die Variable c vom Typ Container . Den Vektor, den die Komponente content speichert, erhält sie in Zeile 13 nach dem Doppelpunkt. Ihn erzeugt ebenfalls ein Makro. Zeile 16 bringt anhand der bekannten Punktnotation noch die Methode print_content() ins Spiel. Übersetzt ein Entwickler nun die Datei impl.rs über rustc impl.rs und führt sie anschließend aus, erscheint die Meldung "Der Container speichert 3 Floats" in der Shell.
Traits
Traits(öffnet im neuen Fenster) dienen der Code-Abstraktion über die Grenzen von Datentypen hinweg. Sie sorgen dafür, dass Entwickler generische Funktionen ähnlich wie die Typklassen aus der funktionalen Programmiersprache Haskell(öffnet im neuen Fenster) nutzen können.
Der folgende Code erzeugt den Trait Dimension , der in Zeile 2 lediglich die Signatur fn volume(&self) -ᐳ f64; der Methode volume() festlegt. Implementierungen folgen in Zeile 9 für eine Kugel (Datentyp Sphere ) und in Zeile 15 für einen Würfel (Datentyp Cube ).
Körperberechnungen (trait.rs)
Die Signatur (Zeile 2) spielt eine Rolle, weil sie sich auf die Methode volume() auswirkt, die der Code ab Zeile 10 für den Datentyp Sphere definiert. Zeile 11 berechnet das Kugelvolumen und liest den benötigten Radius aus dem gleichnamigen Feld. Für den Datentyp Cube wird in den Zeilen 19 bis 23 analog vorgegangen.
Die Hauptroutine des Programms ab Zeile 33 ruft die generische Funktion print_volume() jeweils mit einem Wert für die Datentypen Sphere und Cube auf. Hierbei lässt sich jeder Wert übernehmen, dessen zugehörigen Datentyp der Trait Dimension implementiert. Um dies zu vereinbaren, wird in Zeile 25 nach der abstrakten Typvariablen T und dem Doppelpunkt der Name des Trait genannt.
Ruft das Programm dann print_volume() auf, kommt die Methode volume() mit dem jeweils passenden Datentyp zum Einsatz. Das Ergebnis gibt wieder das Makro println aus.
Makros, Cargo und Ausblick
Makros generieren Code zur Übersetzungszeit, was den Programmieraufwand bisweilen erheblich reduziert. Diese werden wie etwa println ähnlich wie Funktionen verwendet. Jedoch folgt dem Namen des Makros ein Ausrufezeichen wie in Zeile 8 im nächsten Beispiel. Noch deutlicher unterscheiden sich Makros von Funktionen aber in ihrer Syntax.
Makros (macro.rs)
Die ersten fünf Zeilen zeigen den Code des aufgerufenen Makros length . Nach dem Schlüsselwort macro_rules! folgt der Name des Makros. Rust gleicht ($expression:expr) bei jedem Aufruf mit dem übergebenen Parameter ab. Das Muster ist jedoch so allgemein, dass es auf jeden Wert passt. Zugleich speichert es den übergebenen Wert in der Metavariablen $expression . Die Angabe :expr qualifiziert den Wert in der Variablen als Rust-Ausdruck.
Verzeichnet Rust beim Abgleich einen Treffer, wertet ihn der Block ab Zeile 2 aus. In Zeile 3 wandelt das Makro stringify den Wert aus der Metavariablen in eine Zeichenkette um, deren Länge wie zuvor println ausgibt. Wer die Datei macro.rs mit dem Befehl rustc macro.rs && ./macro kompiliert und ausführt, sieht als Resultat die Meldung Die Länge des Ausdrucks beträgt: 5.
Cargo
Analog zu Gems unter Ruby oder Pip unter Python hat auch Rust mit Cargo(öffnet im neuen Fenster) ein eigenes Paketverwaltungsprogramm. Es erzeugt und verwaltet Rust-Pakete von der Kommandozeile aus. Derzeit bietet die Rust-Community etwa 2.200 fertige Pakete(öffnet im neuen Fenster) an.
Auf der Webseite des Projekts steht die letzte stabile Binärversion des Rust-Compilers bereit, bei der Installation wird Cargo gleich mitgeliefert. Der Befehl cargo new test baut das Verzeichnissystem als Gerüst für das Rust-Paket test unter dem gleichnamigen Ordner.
Die darin befindliche Datei Cargo.toml sichert die Metadaten zum Paket. Github beschreibt in der Spezifikation das Format der verwendeten Sprache Toml(öffnet im neuen Fenster) . Die Konfigurationsdatei enthält neben dem Namen, der Version und dem Autor gegebenenfalls auch Abhängigkeiten des Pakets im INI-Stil.
Cargo.toml
Die Datei lib.rs speichert den Rust-Code des Pakets, der zu einem späteren Zeitpunkt und mit Hilfe der Anweisung use test; in Rust-Programme eingebunden wird. Anschließend muss in das Paketverzeichnis gewechselt werden, und das Projekt wird über cargo build mit dem Rust-Compiler rustc kompiliert.
lib.rs
Mittels cargo test können Testfunktionen wie beispielsweise die Funktion gt_zero() aus dem Paket herausgefiltert und anschließend ausgeführt werden. Die Rust-Option #[test] in Zeile 1 weist gt_zero() als Testfunktion aus. Sie generiert – unterstützt vom Makro assert – das Testergebnis für den Test 1 > 0 .
Ausblick
Rust ist eine ambitionierte System-Programmiersprache mit einer schnell wachsenden Community. Das Interesse an der Sprache belegen unter anderem die rund 2.300.000 Downloads der öffentlich verfügbaren Rust-Pakete. Neben Cargo stehen mit Solidoak(öffnet im neuen Fenster) oder Rust-DT(öffnet im neuen Fenster) eine IDE sowie ein Eclipse-Plugin für Rust-Entwickler bereit. Wer will, entwirft zudem mit den Webapplication-Frameworks Ironframework(öffnet im neuen Fenster) und Nickel.rs(öffnet im neuen Fenster) schon jetzt erste Webanwendungen.
Welchen Marktanteil Rust längerfristig erringen kann, hängt aber sicherlich auch vom erfolgreichen Einsatz im Mozilla-Projekt ab. Eine erste stabile Version von Firefox, die auf Rust-Code basiert, wäre sicherlich ein willkommener Einstieg für den Newcomer.



