Aus Linux-Magazin 10/2012

Die Programmiersprache Rust

© Dimitri Surkov, 123RF.com

Von der Öffentlichkeit weitgehend unbeachtet arbeitet die Mozilla Foundation an einer eigenen Programmiersprache. Rust soll das Schreiben von zuverlässigen und schnellen nebenläufigen Anwendungen erleichtern. Zu diesem Zweck machen die Entwickler großzügig Anleihen bei anderen Sprachen.

Laut Mozilla-CTO Brendan Eich entsteht mit Rust die Programmiersprache der Zukunft: Einerseits biete sie mehr Sicherheit als C++, zum anderen soll mit ihr umfassende Parallelisierung in den Webbrowser einziehen [1]. Der Entwickler Graydon Hoare begann die Arbeit an Rust bereits 2006, drei Jahre später übernahm die Mozilla Foundation das Ruder.

Dennoch dauerte es bis zum Januar 2012, ehe die erste Version mit der Nummer 0.1 erschien. Wie die zaghafte Nummerierung schon andeutet, ist Rust immer noch in einem recht frühen Stadium, Syntax und Semantik können sich noch in allen Teilen ändern. Die folgenden Ausführungen basieren auf der Version 0.3 vom 12. Juli 2012 ([2], Abbildung 1).

Abbildung 1: Die Programmiersprache Rust und ihre Implementierung befinden sich noch im Alphastadium.

Abbildung 1: Die Programmiersprache Rust und ihre Implementierung befinden sich noch im Alphastadium.

Kompiliert

Entgegen allen aktuellen Trends laufen in Rust geschriebene Programme nicht in einer virtuellen Maschine, ein Compiler übersetzt sie in nativen Maschinencode. Dank eingebauter Garbage Collection und des so genannten Reference Counting muss sich der Programmierer dennoch nicht selbst mit der Speicherverwaltung herumschlagen.

Rust mischt imperative, objektorientierte und funktionale Programmierparadigmen. Die meisten Anleihen stammen aus der funktionalen Programmierung sowie aus C. So dient auch in Rust eine »main()« -Funktion als Einsprungspunkt ins Programm. Ein einfaches Hallo-Welt-Beispiel sieht daher wie folgt aus:

fn main() {
 io::println("Hallo Welt!");
}

Lokale Variablen deklariert ein vorangestelltes »let« :

let hallo = "Hallo Welt!";

Eine solche Variable ist standardmäßig unveränderlich. Möchte ihr der Programmierer später einen anderen Wert zuweisen, muss er das explizit mit dem Schlüsselwort »mut« anzeigen:

let mut zaehler = 1;

Den Typ einer Variablen ermittelt Rust normalerweise automatisch. Gibt man ihn dennoch an, steht er im Gegensatz zu vielen anderen statisch typisierten Sprachen hinter dem Variablennamen, abgetrennt durch einen Doppelpunkt:

let mut zaehler: int = 1;

Während Strings in der UTF-8-Zeichenkodierung vorliegen müssen, hat der Rust-Programmierer bei Zahlen die Wahl zwischen verschiedenen Bitgrößen. So ist etwa »zaehler« in

let mut zaehler: i16 = 1;

eine 16 Bit große Integerzahl. Die Größe von »int« orientiert sich dagegen wie in C an der Zielarchitektur.

Vektoren, in anderen Sprachen als Arrays bekannt, erzeugt der Entwickler so:

let x: ~[mut str] = ~[mut "Es", "war","schlecht"];

In diesem Fall entsteht ein Vektor »~[]« , der Strings (»str« ) enthält. Diese darf der Programmierer allesamt nachträglich verändern (»mut« ). Die Anweisung »x[2] = “gut”;« verbessert das dritte Wort. Da Rust die Elemente des Vektors mit 0 beginnend nummeriert, greift »[2]« auf das dritte Element des Vektors zu.

Funktionsdefinitionen sehen in Rust etwas merkwürdig aus:

fn ggT(x: int, y: int) -> int { ... }

Das Schlüsselwort »fn« leitet die Funktionsdefinition ein. In den runden Klammern stehen die zu übergebenden Parameter. Im Gegensatz zu anderen Sprachen notiert der Rust-Anwender auch hier erst den Variablennamen und dann den zugehörigen Typ. Welchen Datentyp die Funktion zurückliefert, steht hinter dem Pfeil »->« . Diese Notation ist notwendig, da in Rust eine Funktion mehrere Werte zurückgeben kann:

fn beispiel() -> int, str, float { ... }

Die Kontrollstrukturen »if« und »while« ähneln schon wieder mehr ihren C-Vorbildern, verzichten aber auf die runden Klammern um die Bedingung. Listing 1 zeigt dafür ein Beispiel.

Listing 1

Größter gemeinsamer Teiler

01 fn ggt(c: int, d: int) -> int {
02         let mut a = c;
03         let mut b = d;
04
05         if a==0 { ret b; }
06         else {
07                 while b != 0 {
08                         if a > b {
09                                 a = a-b;
10                         }
11                         else {
12                                 b = b-a;
13                         }
14                 }
15         }
16         ret a;
17 }
18
19 fn main() {
20         let ergebnis = ggt(16,6);
21
22         io::println(#fmt("%d", ergebnis));
23 }

Angemerkt

Kommentare stehen in Rust zwischen »/*« und »*/« . Zudem darf der Entwickler Funktionen mit so genannten Annotationen ausstatten, die Meta-Informationen angeben. Daneben können diese Anmerkungen sogar bestimmte Aktionen steuern. Zum Beispiel würde in

#[cfg(windows)]
fn nur_fuer_windows() { ... }

die Funktion nur unter Windows übersetzt. Die Annotation »#[cfg(windows)]« leistet hier das Gleiche wie ein entsprechendes »#ifdef« in C.

Makros

In Listing 1 kommt in Zeile 22 mit »#fmt« ein Makro zum Einsatz. Der Compiler ersetzt mit »#« beginnende Textkürzel durch einen längeren, vom Benutzer festgelegten Codeblock. Diese Fähigkeit ist jedoch noch nicht vollständig implementiert, derzeit bringt der Compiler nur wenige eingebaute Makros mit. Dazu zählt auch »#fmt« , das zur Formatierung von Texten dient und ähnlich wie »printf()« aus C arbeitet. Den Rückgabewert einer Funktion legt »ret« fest.

In Rust kann auch ein Block in geschweiften Klammern einen ganz bestimmten Wert annehmen:

let x = if a < 1 { 5 } else { 6 };

Abhängig von der Bedingung speichert Rust in der Variablen »x« den Wert »5« oder »6« . Fehlt – wie im folgenden Beispiel zu sehen – das Semikolon in der (letzten) Anweisung eines Blocks, ist der ganze Block gleich dem Wert dieses Ausdrucks. Das funktioniert auch in Funktionen:

fn kleinerzwei(x: int) -> bool { x < 2 }

In diesem Beispiel liefert die Funktion einen Wahrheitswert zurück.

Abgeschlossen

In Rust lassen sich Funktionen in Variablen speichern, an Funktionen übergeben oder von solchen zurückliefern. Darüber hinaus kennt Rust das Konzept der Closures, die wiederum das Beispiel in Listing 2 ermöglichen. Zunächst nimmt »macheinefunktion()« eine Zahl entgegen (den »multiplikator« ) und liefert dann eine Funktion zurück, die aus einer Integerzahl eine andere macht:

Listing 2

Closures

01 fn macheinefunktion(multiplikator: int) -> fn@(int) -> int {
02
03         ret fn@(zahl: int) -> int {
04                 ret zahl * multiplikator;
05         };
06 }
07
08 fn main() {
09
10         let verdopple = macheinefunktion(2);
11         let ergebnis = verdopple(5);
12
13         io::println(#fmt("%d", ergebnis));
14 }
fn@(int) -> int

Diese neue Funktion bastelt »macheinefunktion()« in Zeile 4 zusammen. Sie multipliziert später einfach die ihr übergebene »zahl« mit dem »multiplikator« . Die »main()« -Funktion lässt auf diesem Weg eine Funktion erstellen, die alles mit »2« multipliziert. Die resultierende Funktion wiederum landet in der Variablen »verdopple« . Die nächste Zeile ruft die Funktion mit dem Wert »5« auf und speichert den Rückgabewert in der Variablen »ergebnis« .

Das »@« hinter dem »fn« sorgt dafür, dass die neue Funktion den Wert von »multiplikator« dauerhaft behält. Andernfalls würde er mit dem Ende von »macheinefunktion« ins Nirwana verschwinden.

Generisches

Die folgende Funktion liefert etwa die Anzahl der Elemente in einem Vektor »v« :

fn anzahl(v: ~[int]) -> int {
 ret vec::len(v);
}

Dieser darf allerdings nur Integerwerte enthalten. Allgemeiner formuliert sieht die Funktion so aus:

fn anzahl<T>(v: ~[T]) -> int {
 ret vec::len(v);
}

Sie besitzt jetzt einen Platzhalter »T« , der für einen beliebigen einzusetzenden Datentyp steht. Damit liefert sie die Länge jedes beliebigen Vektors zurück. Die ganze Funktion bezeichnet man als generisch. Die Notation in spitzen Klammern dürfte C++-Programmierern von den Templates bekannt sein.

Fasst der Rust-Anwender mehrere Variablen in geschweiften Klammern zusammen, erhält er einen so genannten Record:

type punkt = {mut x: int, mut y: int};

Hier entsteht zunächst ein neuer Record, der aus den zwei Variablen »x« und »y« besteht. Anschließend erhält dieser Datentyp den Namen »punkt« . Ab sofort lässt sich ein neuer »punkt« wie ein eingebauter Datentyp verwenden:

let a: punkt = {mut x: 3, mut y: 7};

Die Variable »a« enthält den Record, dessen Variablen »x« mit dem Wert »10« und »y« mit dem Wert »7« belegt sind. Auf eine Variable in diesem Record greift der Rust-Programmierer über die Punkt-Notation zu: In »a.x = 1;« erhält die Variable »x« im Record »a« den Wert »1« . Benötigt der Entwickler nur einen einzigen Record, kann er sich die Typdefinition via »type« auch sparen und stattdessen direkt einen Record in der Variablen ablegen:

let a = {mut x: 3, mut y: 7};

Natürlich kennt die Sprache auch generische Datentypen:

type messung<T> = {wert: T, nummer: int};
let test: messung<int> = {wert: 12,nummer: 1};

In Rust sind Klassen Records mit einem Namen, die Variablen und Methoden enthalten. Methoden sind dabei Funktionen, die über das Schlüsselwort »self« die Variablen in ihrer eigenen Klasse manipulieren.

Klassenkampf

Listing 3 zeigt ein Beispiel für eine Klassendefinition. Dabei ist »new« der Konstruktor, den Rust automatisch beim Anlegen eines Objekts aufruft:

Listing 3

Beispiel für eine Klasse in Rust

01 class punkt {
02         let x: int;
03         let y: int;
04         new(x: int, y: int) { self.x = x; self.y = y; }
05 }
let a: punkt = punkt(3,7);

Zusätzlich gibt es die so genannten Interfaces. Ein Interface ist zunächst eine Sammlung von – Achtung! – Methoden:

iface flaeche {
 fn flaeche() -> int;
}

In diesem Fall gibt es ein Interface »flaeche« mit einer Methode »flaeche()« . Diese Methode führt auf einem Objekt irgendwelche Berechnungen durch und gibt eine Integerzahl aus. Für ausgewählte Objekte kann der Entwickler eine konkrete Implementierung angeben, beispielsweise »impl of flaeche for rechteck« in den Zeilen 16 bis 22 in Listing 4..In diesem Fall berechnet die Funktion »flaeche()« den Flächeninhalt für eine Instanz von »rechteck« .

Listing 4

Interfaces

01 class rechteck {
02         let weite: int;
03         let hoehe: int;
04         new(w: int, h: int) { self.weite = w; self.hoehe = h;}
05 }
06
07 class quadrat {
08         let laenge: int;
09         new(l: int) { self.laenge = l;}
10 }
11
12 iface flaeche {
13         fn flaeche() -> int;
14 }
15
16 impl of flaeche for rechteck {
17         fn flaeche() -> int { self.weite * self.hoehe }
18 }
19
20 impl of flaeche for quadrat {
21         fn flaeche() -> int { self.laenge * self.laenge }
22 }
23
24 fn main() {
25         let kasten: rechteck = rechteck(3,4);
26         let ergebnis: int = kasten.flaeche();
27         io::println(#fmt("%d", ergebnis));
28 }

Das komplette Beispiel in Listing 4 zeigt noch eine weitere Implementierung für ein »quadrat« . In seiner »main()« -Funktion erstellt es dann ein konkretes Rechteck namens »kasten« und lässt schließlich dessen Fläche berechnen:

let ergebnis = kasten.flaeche();

Rust stellt anhand der Klasse selbst fest, welche der beiden »flaeche()« -Implementierungen hier aufzurufen ist. Übrigens könnte man auch für die eingebauten Datentypen Implementierungen von »flaeche« erstellen (auch wenn das hier sinnlos ist):

impl of flaeche for int {
 fn flaeche() -> int { self * self }
}

Damit würde »s.flaeche();« die Zahl 25 als Ergebnis zurückliefern. Der Ganzzahl 5 hängt man in der Rust-Schreibweise einfach die Funktion an.

Soll das eigene Programm eine Aufgabe parallel erledigen, lagert der Programmierer sie einfach in eine separate Task aus:

do task::spawn {
 log(error, "Hello,World!");
};

Mit der dabei angelegten Task lassen sich Informationen über einen Kommunikationskanal austauschen. Listing 5 zeigt ein Beispiel.

Listing 5

Kommunikation mit einer Task

01 fn main() {
02     let port = comm::port();
03     let kanal = comm::chan(port);
04     do task::spawn {
05         let ergebnis = 2+3;
06         comm::send(kanal, ergebnis);
07     };
08
09     /* weitere aufwändige Berechnungen */
10
11     let ergebnis = comm::recv(port);
12
13     io::println(#fmt("%d", ergebnis));
14 }

Es erstellt zunächst eine neue Empfangsstelle für die Kommunikation, genannt Port. Die nächste Zeile verbindet diesen Port mit einem neuen Kommunikationskanal. Als Nächstes startet die Task, die lediglich »2+3« ausrechnet und das Ergebnis in den Kanal schiebt. Das Hauptprogramm empfängt diesen Wert am Port (Zeile 11) und druckt ihn anschließend auf dem Bildschirm aus.

Rust-Compiler in Rust

Zusammen mit der Sprachspezifikation stellt die Mozilla Foundation auch einen Compiler bereit. Diese Referenzimplementierung besteht aus einem Frontend für das Compilersystem LLVM und ist selbst in Rust geschrieben. Um die aktuelle Version des Rust-Compilers zu erstellen, benötigt man folglich einen älteren, bereits fertigen Rust-Compiler.

Das klingt jedoch komplizierter, als es tatsächlich ist. Um den Compiler in Betrieb zu nehmen, installiert der Anwender über den Paketmanager: »g++« ab Version 4.4, Python 2.6 oder höher, Perl ab Version 5.0, Make und Curl. Anschließend lädt er das aktuelle Archiv von der Rust-Homepage [1], entpackt es, stellt sicher, dass eine Internetverbindung besteht, und übersetzt den Quellcode schließlich mit dem üblichen »./configure; make; sudo make install« .

Übersetzen

Ein in Rust geschriebenes Programm packt man in eine Datei mit der Endung ».rs« und übergibt diese an den Compiler »rustc« . Der Befehl

rustc beispiel.rc

erstellt ein ausführbares Programm namens »beispiel« .

Längeren Quellcode verpackt der Rust-Programmierer wie in Listing 6 in einzelne Module – in C++ würde man von Namensräumen sprechen. Das dortige Beispiel fasst die beiden Funktionen »auto()« und »lkw()« im Modul »garage« zusammen. Auf eine Funktion in einem Modul greift man über zwei Doppelpunkte zu: »garage::auto()« .

Listing 6

Module

01 mod garage {
02     fn auto() -> str { "5-Türer" }
03     fn lkw() -> str { "2-Türer" }
04 }
05
06 fn main() {
07     io::println(garage::auto());
08 }

Den Code großer Programme verteilt der Entwickler gewöhnlich auf mehrere ».rs« -Dateien. Rust bezeichnet dabei eine zusammengehörende Gruppe von ».rs« -Dateien als Crate (Kiste). Besteht das eigene Programm nur aus einer ».rs« -Datei, bildet diese eine einzige große Crate. Daneben kann der Programmierer eine Crate aus mehreren ».rs« -Dateien zu einer dynamischen Bibliothek schnüren. Dazu legt er eine spezielle ».rc« -Datei an, die alle gewünschten Dateien einbindet. Ein Beispiel für eine ».rc« -Datei zeigt Listing 7.

Listing 7

Einfache .rc-Datei

01 #[link(name = "garage", vers = "2.5", author = "Tim")];
02 #[crate_type = "lib"];
03 mod fuhrpark {
04     mod autos;
05     mod lkws;
06 }

Code in Kisten

Dort bindet der Compiler die beiden Dateien »autos.rs« und »lkws.rs« ein. Aufgrund der Verschachtelung erwartet er sie im Unterverzeichnis »fuhrpark« . Dabei landen die Inhalte aus »autos.rs« und »lkws.rs« automatisch in den Modulen »fuhrpark::autos« und »fuhrpark::lkws« . Aus dem so zusammengefügten Quellcode erzeugt der Compiler normalerweise das ausführbare Programm.

In Listing 7 zwingt ihn die Angabe »#[crate_type = “lib”];« jedoch dazu, eine dynamische Bibliothek zu erstellen. Diese bindet die Anweisung »use« in ein Rust-Programm ein:

use fuhrpark;

»#[link(…)]« liefert dem Compiler schließlich noch ein paar Meta-Informationen zur ».rc« -Datei.

Der Rust-Tarball enthält bereits zwei Standardbibliotheken: Während die Core-Bibliothek [3] Funktionen zur Manipulation der eingebauten Datentypen mitbringt, bietet die Std-Bibliothek [4] ein paar nützliche High-Level-Funktionen, etwa zum Zugriff auf ein Netzwerk oder zum Berechnen eines SHA-1-Hashwerts. Da Rust C-kompatiblen Maschinencode erzeugt, lassen sich sogar bestehende C-Bibliotheken einbinden und nutzen.

Protokollant

Rust besitzt einen eingebauten und äußerst nützlichen Logging-Mechanismus. So führt der Funktionsaufruf

log(warn, "Sei gewarnt!");

zur Ausgabe von Abbildung 2. Die Funktion »log« verlangt zuerst den Loglevel, also die Dringlichkeit der Meldung, zweitens die zu loggende Nachricht. Bereits vorgegeben sind die Loglevel »debug« , »info« , »warn« und »error« , weitere darf der Entwickler selbst definieren.

Abbildung 2: Rust loggt die Nachricht »Sei gewarnt!« – allerdings nur, wenn der Programmierer zuvor die Umgebungsvariable »RUST_LOG« auf den Namen des Programms setzt.

Abbildung 2: Rust loggt die Nachricht »Sei gewarnt!« – allerdings nur, wenn der Programmierer zuvor die Umgebungsvariable »RUST_LOG« auf den Namen des Programms setzt.

Rust soll sich offenbar zu einer eierlegenden Wollmilchsau entwickeln: Der Code will die Geschwindigkeit von C++ erreichen, eine Speicherverwaltung verhindert nervende Abstürze, die funktionale Programmierung vereinfacht nebenläufige Aufgaben. Um das alles zu erreichen, übernehmen die Entwickler die ihrer Meinung nach besten Konzepte aus anderen Sprachen. Einfluss genommen haben laut Sprachreferenz so illustre Gestalten wie NIL, Hermes, Erlang, Sather, Newsqueak, Napier, Go, SML, C#, C++, Haskell, Python und Ruby. Die erstgenannten Exoten stammen durchweg aus den 80er Jahren.

Das Ergebnis ist ein Programmiersprachen-Cocktail, in dem einige Konzepte – wie etwa die Interfaces – noch aufgesetzt wirken. Fast alle Rust-Neueinsteiger müssen umlernen, nur wer von funktionalen Programmiersprachen kommt, trifft auf viel Bekanntes.

Kryptisch

Rust erlaubt zwar recht kompakten Quellcode, der dafür aber geradezu kryptisch und somit schwer verständlich wirkt. So muss man häufig erst zweimal hinschauen, bevor sich die Arbeitsweise einer Funktion erschließt: Wer könnte schon auf Anhieb sagen, was die folgende Beispielfunktion aus dem Rust-Tutorial macht:

fn mk_appender(suffix: str) -> fn@(str)-> str {
 ret fn@(s: str) -> str { s + suffix };
}

Kleiner Tipp: Sie ist mit Listing 2 verwandt.

Zu allem Überfluss ist auch noch die Dokumentation der Programmiersprache stark lückenhaft (Abbildung 3). Das gilt selbst für die Sprachreferenz, aus der sich einige Codebeispiele zumindest mit dem aktuellen Compiler überhaupt nicht übersetzen lassen.

Abbildung 3: Die Rust-Dokumentation ist ziemlich lückenhaft.

Abbildung 3: Die Rust-Dokumentation ist ziemlich lückenhaft.

Fazit

Besonders eilig scheint es Mozilla nicht zu haben: Selbst nach sechs Jahren Entwicklungszeit dümpelt Rust unfertig vor sich hin, Teile der Spezifikation verändern sich immer noch. Da verwundert es nicht, dass in Rust geschriebene Programme noch ausstehen. Ob das Konzept dieser Sprachmixtur aufgeht, muss die Browser-Firma jedenfalls erst noch beweisen.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 5 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben