Mit Version 1.0 legte Mozillas System-Programmiersprache Rust Mitte Mai nach längerer Entwicklungszeit eine erste Verschnaufpause ein. Ob sie sich im weiteren Rennverlauf gegen C und C++ behaupten kann, hängt außer von Mozillas Engagement auch davon ab, ob die Sprache mit ihren Features punktet.
Die statisch getypte Programmiersprache Rust [1] legt viel Wert auf Schnelligkeit und Sicherheit. Rust speichert, wie ihr großes Vorbild C, Variablen fester Größe in einem Stack. Zeigerwerte legt es hingegen im Heap des Hauptspeichers ab. Der Code in Listing 1 demonstriert den kontrollierten Umgang mit alloziertem Speicher unter Rust.
Listing 1
Geänderte Besitzverhältnisse (moved_type.rs)
01 fn print_length(vec:Vec<i32>) {
02 println!("Der Länge des Vektors beträgt {}", vec.len());
03 }
04
05 fn main() {
06 let mut vec:Vec<i32> = vec![1,2,3];
07 print_length(vec);
08 vec.push(4);
09 }
Speichert der Entwickler Listing 1 in der Datei »moved_type.rs« und kompiliert die Software anschließend über »rustc moved.rs« in der Shell, bricht der Compiler den Vorgang mit der Fehlermeldung »moved_type.rs:8:3: 8:6 error: use of moved value: `vec`« für die Zeile 8 ab.
In Listing 1 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 diese so zum exklusiven Besitzer. In der Konsequenz darf das Programm den Zeiger ab Zeile 7 nicht mehr verwenden, was zu der Fehlermeldung führt.
Will der Entwickler den Zeiger dennoch weiterverwenden, kann er ihn temporär an »print_length()« verleihen (Borrowing). Dazu übergibt er in Zeile 7 von Listing 1 eine Referenz (»&vec« ) und modifiziert analog die Funktionsdeklaration von »print_length()« in Zeile 1 nach »vec: &Vec<i32>« . Soll »print_length()« zudem in der Lage sein, den Zeigerwert zu ändern, greift er alternativ zum Fragment »vec: &mut Vec<i32>« .
Rust erlaubt es dem Entwickler aber nur, eine veränderbare Referenz zu übergeben. Will er den Ressourcenverleih in jedem Aspekt der Programmierung richtig machen, bietet Rust neben dem Borrowing die Angabe von Lifetimes.
Beim Freigeben von alloziertem Speicher verfolgt Rust einen eigenen Ansatz: Das Rust-Kompilat erledigt die Speicherfreigabe selbsttätig und verzichtet dabei auf einen Garbage Collector wie unter Java. Auch die Funktion »free()« [2], 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 besitzen 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 Listing 2 ermittelt das Maximum zweier ganzer Zahlen, die sie in der Parameterliste nach dem Funktionsnamen in Zeile 1 übernimmt. 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.
Listing 2
Rückgabewerte
01 fn max(a:i32, b:i32) -> i32 {
02 if a > b { return a };
03 b
04 }
Zeile 2 verwendet das Schlüsselwort »return« (»ret« in Version 0.3) und gibt »a« als Resultat zurück. Andernfalls gibt Zeile 3 den Wert von »b« zurück. Die letzte Zeile des Funktionsrumpfes darf auf »return« verzichten.
Schlösse der Programmierer Zeile 3 irrtümlicherweise mit einem Semikolon ab, 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
Rust bedient sich ausgiebig im Repertoire anderer Sprachen. So verwendet sie, wie die funktionalen Programmiersprachen Haskell [3] und Standard ML [4], 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 aus Listing 3 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.
Listing 3
Mustererkennung
01 let w = match x {
02 0 => y+z,
03 _ => y*z
04 }
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 von Listing 4 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« .
Listing 4
Anwendungsdaten ohne Klassen (impl.rs)
01 struct Container {
02 content: Vec<f64>
03 }
04
05 impl Container {
06 fn print_content(&self) {
07 println!("Der Container speichert {} Floats", self.content.len());
08 }
09 }
10
11 fn main() {
12 let c = Container {
13 content: vec!(1.0, 2.0, 3.0)
14 };
15
16 c.print_content();
17 }
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 diese anschließend aus, erscheint die Meldung »Der Container speichert 3 Floats« in der Shell.
Traits
Traits 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 [5] nutzen.
Listing 5 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« ).
Listing 5
Körperberechnungen (trait.rs)
01 trait Dimension {
02 fn volume(&self) -> f64;
03 }
04
05 struct Sphere {
06 radius: f64
07 }
08
09 impl Dimension for Sphere {
10 fn volume(&self) -> f64 {
11 4.0/3.0 * std::f64::consts::PI * cubic(self.radius)
12 }
13 }
14
15 struct Cube {
16 side: f64
17 }
18
19 impl Dimension for Cube {
20 fn volume(&self) -> f64 {
21 cubic(self.side)
22 }
23 }
24
25 fn print_volume<T: Dimension>(obj: T) {
26 println!("Das Objekt hat das Volumen von {}", obj.volume());
27 }
28
29 fn cubic(x: f64) -> f64 {
30 x * x * x
31 }
32
33 fn main() {
34 print_volume(Sphere { radius: 1.0});
35 print_volume(Cube { side: 1.0 });
36 }
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« (Würfel) geht der Entwickler in den Zeilen 19 bis 23 analog vor.
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, nennt der Programmierer in Zeile 25 nach der abstrakten Typvariablen »T« und dem Doppelpunkt den Namen des Trait.
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
Makros generieren Code zur Übersetzungszeit, was den Programmieraufwand bisweilen erheblich reduziert. Solche wie »println« verwendet der Entwickler ähnlich wie Funktionen. Jedoch folgt dem Namen des Makros ein Ausrufezeichen wie in Zeile 8 von Listing 6. Noch deutlicher unterscheiden sich Makros in ihrer Syntax von den Funktionen.
Listing 6
Makros (macro.js)
01 macro_rules! length {
02 ($expression:expr) => (
03 println!("Die Länge des Ausdrucks beträgt: {}", (stringify!($expression)).len())
04 )
05 }
06
07 fn main() {
08 length!(7 + 4);
09 }
Die ersten fünf Zeilen von Listing 6 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.js« mit dem Befehl »rustc macro.js && ./macro« kompiliert und ausführt, sieht als Resultat die Meldung »Die Länge des Ausdrucks beträgt: 5« .
Cargo
Analog zu Gems [6] unter Ruby oder Pip [7] unter Python besitzt auch Rust mit Cargo [8] ein eigenes Paketverwaltungsprogramm, es erzeugt und verwaltet Rust-Pakete von der Kommandozeile aus. Derzeit bietet die Rust-Community etwa 2200 fertige Pakete unter [9] an (Abbildung 1). Der Befehl
curl -sSf https://static.rust-lang.org/rustup.sh | sh
installiert nicht nur die letzte stabile Binärversion des Rust-Compilers unter Linux, sondern Cargo gleich mit. Der Befehl »cargo new test« baut das Verzeichnissystem aus Listing 7 als Gerüst für das Rust-Paket »test« unter dem gleichnamigen Ordner.
Listing 7
Typische Verzeichnisstruktur eines Rust-Pakets
01 test 02 |- Cargo.toml 03 |- src 04 |- lib.rs
Die Datei »Cargo.toml« (Listing 8) sichert die Metadaten zum Paket. Github beschreibt in der Spezifikation das Format der verwendeten Sprache Toml [10]. Die Konfigurationsdatei enthält neben dem Namen, der Version und dem Autor gegebenenfalls auch Abhängigkeiten des Pakets im INI-Stil.
Listing 8
Cargo.toml
01 [package] 02 name = "test" 03 version = "0.1.0" 04 authors = ["pa"]
Die Datei »lib.rs« (Listing 9) speichert den Rust-Code des Pakets, den der Entwickler zu einem späteren Zeitpunkt und mit Hilfe der Anweisung »use test;« in Rust-Programme einbindet. Anschließend wechselt er mit »cd test« in das Paketverzeichnis und kompiliert das Projekt über »cargo build« mit dem Rust-Compiler »rustc« .
Listing 9
lib.rs
01 #[test]
02 fn gt_zero() {
03 assert!(1>0);
04 }
Mittels »cargo test« filtert er Testfunktionen wie beispielsweise die Funktion »gt_zero()« (Listing 9) aus dem Paket heraus und führt sie anschließend aus. 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« in Abbildung 2.
Fazit
Rust ist eine ambitionierte System-Programmiersprache mit einer schnell wachsenden Community. Das Interesse an der Sprache belegen unter anderem die rund 2300000 Downloads der öffentlich verfügbaren Rust-Pakete. Neben Cargo stehen mit Solidoak (Abbildung 3, [11]) oder Rust-DT [12] eine IDE sowie ein Eclipse-Plugin für Rust-Entwickler bereit. Wer will, entwirft zudem mit den Webapplication-Frameworks Ironframework [13] und Nickel.rs [14] 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 weiterer Boxenstopp für den Newcomer.
Infos
- Rust: http://www.rust-lang.org
- Die »free()« -Funktion: http://home.fhtw-berlin.de/~junghans/cref/FUNCTIONS/free.html
- Haskell: http://haskell.org
- Standard ML: http://www.smlnj.org
- Typklassen in Haskell: http://learnyouahaskell.com/types-and-typeclasses
- Gems für Ruby: https://rubygems.org
- Pythons Paketmanager Pip: https://pypi.python.org/pypi/pip
- Cargo-Guide: http://doc.crates.io/guide.html
- Cargos zentrale Webseite: https://crates.io
- Toml: https://github.com/toml-lang/toml
- Solidoak: https://github.com/oakes/SolidOak
- Rust-DT: https://github.com/RustDT/RustDT
- Ironframework: http://ironframework.io
- Nickel: http://nickel.rs








