Aus Linux-Magazin 07/2018

Erste Schritte mit dem Rust-Webframework Rocket

© Konstantin Shaklein, 123RF

Die Heimat von Rust, Mozillas System-Programmiersprache mit Fokus auf Typsicherheit und Geschwindigkeit, vermuten viele eher im Hardware-nahen Umfeld. Dass sich damit auch Webapplikationen schreiben lassen, will das noch junge Webframework Rocket beweisen. Der Artikel stellt es vor.

Geht es um Server-seitige Webentwicklung, denken viele zunächst an Sprachen wie Javascript, PHP und Ruby oder an Frameworks wie Express.js, Symphony und Ruby on Rails. Wer Abseits des Mainstreams sucht, findet im noch jungen Framework Rocket [1] eine schnelle und einfache Möglichkeit, Webentwicklung in der Programmiersprache Rust ([2], [3]) zu betreiben. Diese spielt im Backend ihre Stärken wie Typsicherheit und hohe Geschwindigkeit aus.

Die Anforderungen an Webapplikationen sind in den vergangenen zehn bis zwanzig Jahren stetig gestiegen – und damit auch deren Komplexität. Inzwischen spielt sich dank hochoptimierter Javascript-Engines zwar vieles Client-seitig im Browser ab, die Webserver entlastet das aber meist nicht.

Die sollen oft riesige Datenmengen ausliefern, parallel Tausende von Verbindungen halten und zugleich möglichst ohne Verzögerung auf Anfragen reagieren. All das bitte ohne Sicherheitslücken und mit niedrigen Entwicklungskosten. Da wundert es nicht, dass eine große Auswahl an Webframeworks den Entwicklern unter die Arme greifen möchte.

Das gilt auch für das Rocket-Framework, mit dem Webentwickler robuste und schnelle Webapplikationen schreiben sollen. Es verfolgt den Anspruch, dass sich Entwickler nicht wiederholen oder so genannten Boilerplate-Code verfassen müssen, den die Programmiersprache erfordert, ohne dass er eine Rolle für die Anwendung selbst spielt.

Die Programmiersprache Rust

Rust ist eine von Mozilla Research gesponserte Multiparadigmen-Systemprogrammiersprache. Damit ist gemeint, dass sich Rust für die Programmierung auf Hardware-naher Ebene eignet: Mikrocontroller, Treiber, Betriebssysteme und dergleichen. Das ist möglich, da Rust (wie C und C++) ohne Garbage Collector auskommt und (analog zu C++) auf das Konzept der Zero Cost Abstractions [4] setzt.

Doch Rust möchte nicht nur als direkter Konkurrent zu C und C++ auftreten, sondern hat noch weitaus mehr zu bieten. Zu nennen sind unter anderem der eingebaute Paket-Manager und -Builder Cargo [5], viele moderne Sprachfeatures (etwa Closures, Iteratoren, Typinferenz), Trait-basierte Abstraktionen und mehr. Beliebt scheint Rust nicht nur bei Systemprogrammierern, sondern auch bei Python-, Ruby- oder Javascript-Entwicklern zu sein. 2018 wählten die Entwickler auf Stack Overflow sie in einer Umfrage zum dritten Mal in Folge zur “Most loved Programming Language”.

Von anderen Sprachen will sich Rust vor allem durch einen eigenen Ansatz in Sachen Sicherheit und Robustheit abgrenzen: Akzeptiert der Compiler ein Programm, ist es garantiert frei von bestimmten klassischen Programmierfehlern, etwa Buffer Overflows und Data Races. Auch Logikfehler fängt Rust nach Möglichkeit bereits beim Kompilieren ab. So sollen Entwickler ihrem Programm vertrauen können und auch an größeren Systemen ohne Furcht Änderungen vornehmen.

Technisch setzt Rust diese Idee durch ein starkes Typsystem um. So genannte Borrow Checker achten darauf, dass eine Anwendung niemals Referenzen auf ungültigen Speicher enthält. All das wird zur Kompilierzeit garantiert, sodass die Ausführungsgeschwindigkeit nicht darunter leidet. Rust lässt sich einfach über die Website [2] installieren, Compiler stehen für alle gängigen Plattformen bereit.

Rocket installieren

Obwohl es wie ein umfangreiches Framework wirkt, besteht Rocket nur aus wenigen Bibliotheken, die in der Rust-Welt Crates heißen. Um Rust (oder besser dem Buildtool Cargo) mitzuteilen, dass der Entwickler Rocket nutzen möchte, gibt dieser die entsprechenden Abhängigkeiten in der zum Projekt gehörenden Datei »Cargo.toml« an (Listing 1).

Listing 1

Cargo.toml

01 [dependencies]
02 rocket = "0.3.9"
03 rocket_codegen = "0.3.9"

Rust muss an diesem Punkt bereits installiert sein. Führt der Programmierer dann im Verzeichnis mit der »Cargo.toml«-Datei die Kommandos »cargo build« oder »cargo run« aus, lädt Cargo Rocket mit all seinen Abhängigkeiten automatisch herunter und kompiliert sie.

Abbildung 1: Das Projektverzeichnis zum "Hello World"-Beispiel. Wichtig sind die Datei »Cargo.toml« und der »src«-Ordner.

Abbildung 1: Das Projektverzeichnis zum “Hello World”-Beispiel. Wichtig sind die Datei »Cargo.toml« und der »src«-Ordner.

Im Vorfeld ist allerdings eine Sache zu beachten: Da Rocket viele »unstable«-Features von Rust nutzt, benötigt der Entwickler zwingend einen Nightly Compiler. Den richten die Befehle

rustup install nightly
rustup override set nightly

ein, wobei der letzte die Nightly-Version als Standard setzt. Das Tool »rustup« landet meist mit dem Rust-Compiler zusammen auf der Festplatte. Es verwaltet unterschiedliche Versionen des Compilers, bringt ihn auf den neuesten Stand und macht Cross-Compiling einfach.

“Hello World”

Einen guten ersten Eindruck des Webframeworks bekommen Entwickler nach dem Installieren von Rust und Rocket durch das minimale “Hello World”-Beispiel in Listing 2. Bauen und führen sie den Code (Abbildung 1) über Cargo aus, erscheint im Browser ein simples »Hallo, Linux Magazin!« (Abbildung 2), sobald sie die URL »localhost:8000« eintippen. Im Terminal zeigt Rocket Informationen zur Konfiguration, zu aktiven Routen und empfangenen Requests an (Abbildung 3).

Listing 2

“Hello World” in Rocket (main.rs)

01 #![feature(plugin)]
02 #![plugin(rocket_codegen)]
03
04 extern crate rocket;
05
06 #[get("/")]
07 fn hello() -> String {
08     "Hallo, Linux Magazin!".into()
09 }
10
11 fn main() {
12     rocket::ignite()
13         .mount("/", routes![hello])
14         .launch();
15 }

Den Einstiegspunkt des Programms stellt wie üblich die »main()«-Funktion dar, die Rocket vorbereitet und startet. Die Funktion legt die Routen sowie – optional – eine Konfiguration an, die im Beispiel jedoch fehlt. Der Aufruf von »launch()« startet eine Schleife, in der Rocket auf eingehende Anfragen wartet.

Abbildung 2: Im Browser sieht der Anwender nach dem Start von Rocket einen freundlichen Gruß.

Abbildung 2: Im Browser sieht der Anwender nach dem Start von Rocket einen freundlichen Gruß.

Das Beispiel verwendet nur eine Route mit dem durch »hello()« definierten Handler. Diese soll die Anfragen an den Wurzelpfad (»/«) abdecken. Da Routen für andere Requests (wie etwa an »localhost:8000/news«) fehlen, liefert Rocket für sie den Fehler 404 zurück. Entwickler erstellen in Rocket also pro Endpunkt (zum Beispiel »/«, »/news« und »/events«) eine Route mitsamt Handler in Form einer einfachen Funktion.

Abbildung 3: Im Terminal zeigt Rocket Informationen an, die Rückschlüsse auf Abfragen im Browser geben.

Abbildung 3: Im Terminal zeigt Rocket Informationen an, die Rückschlüsse auf Abfragen im Browser geben.

Routen definieren

Eine Route besteht aus einer Handler-Funktion und Metadaten, um die eingehenden Requests zuzuordnen. Die Handler-Funktion im Beispiel ist sehr simpel: Sie gibt einfach einen String zurück. Rust liefert das Ergebnis des letzten Ausdrucks einer Funktion automatisch zurück, ein »return« ist nicht nötig.

Die Metadaten einer Route spezifiziert der Entwickler in Rocket mit Hilfe von Attributen (Annotationen der Form »#[]«). Im Beispiel bedeutet »#[get(“/”)]«, dass dieser Handler »GET«-Requests an den Pfad »/« behandeln soll.

Oft möchten Entwickler Routen nicht nur an statische Pfade binden, sondern auch dynamische Parameter in den Pfaden akzeptieren. Das ist einfach möglich, indem sie Platzhalter im Pfad verwenden, deren Werte der Handler als Parameter übergeben bekommt (Listing 3). In diesem Fall sind das der String »name« und der unsignierte 8-Bit-Integer-Typ »age«. Wer nun im Browser die URL »localhost:8000/birthday/Peter/27« aufruft, bekommt die Nachricht »Glückwunsch zu deinem 27. Geburtstag, Peter!« zu sehen.

Listing 3

Dynamische Segmente in Pfaden

01 #[get("/birthday/<name>/<age>")]
02 fn hello(name: String, age: u8) -> String {
03     format!(
04         "Glückwunsch zu deinem {}. Geburtstag, {}!",
05         age,
06         name,
07     )
08 }

Rocket nutzt Rusts vielfältige Werkzeuge zur Metaprogrammierung, um die Attribute zur Kompilierzeit zu verarbeiten. So lässt sich die Korrektheit der Route schon möglichst früh validieren. Stellt es Fehler fest, informiert Rocket den Programmierer. Das geschieht in Form von Ausgaben beim Kompilieren, die den normalen Rust-Kompilierfehlern sehr ähneln. Vertippt sich der Programmierer beispielsweise beim Namen eines dynamischen Parameters, weist Rocket ihn darauf direkt hin.

Daneben validiert Rocket Pfadparameter. So erwartet der Parameter »age« einen Integer-Typ. Was passiert, wenn jemand als Pfad »birthday/Peter/abc« eingibt? Rocket bemerkt, dass es den String »abc« nicht als gültigen Integer parsen kann und bewertet den Handler als nicht benutzbar. In diesem Fall versucht es, andere verfügbare Routen einzuschlagen. Hat der Entwickler aber nicht mehrere Handler für den gleichen Pfad bestimmt, kümmert sich ein besonderer 404-Handler um den Request. Selbstverständlich kann der Webentwickler alle speziellen Error-Handler überschreiben und so selbst definieren.

Request Guards

Zusätzlich zu den Pfadsegmenten kann die Handler-Funktion noch weitere Parameter definieren, die nicht aus dem Pfad, sondern anderen Teilen des HTTP-Requests stammen. Diese Request Guards [6] fungieren als eine Art Türsteher zwischen einer Anfrage und dem Handler, der sie bearbeitet. Rocket gibt Anfragen nur weiter, wenn der Türsteher grünes Licht gibt. Dann lassen sich sinnvolle Daten aus der Anfrage ableiten. Lehnt der Request Guard sie jedoch ab, ruft Rocket den Handler nicht auf.

Ein klassischer Einsatzort für Request Guards ist die Nutzerauthentifizierung (Listing 4). Hier stammt der Typ »User« nicht von Rocket, sondern vom Webentwickler. Damit das klappt, muss er für den Typ ein so genanntes Trait [7] namens »FromRequest« implementieren.

Listing 4

Request Guard in Aktion

01 #[get("/balance")]
02 fn balance(user: User) -> String {
03     format!("Hallo {}, du besitzt zurzeit {}¤.", user.name, user.balance)
04 }

Traits bilden Rusts Hauptwerkzeug für die Abstraktion. Sie sind zwar mit Interfaces aus anderen Sprachen vergleichbar (sie definieren Methodenköpfe, zu denen der implementierende Typ den Rumpf liefern muss), weisen aber ein paar wichtige Unterschiede auf. In Rusts Standardbibliothek gibt es zum Beispiel das Trait »Default« (Typen, für die es einen sinnvollen Standardwert gibt), »io::Read« (Typen, aus denen sich Bytes lesen lassen wie »File« und »TcpStream«) und »Clone« (Typen, deren Instanzen sich klonen lassen).

Rockets Trait »FromRequest« repräsentiert Typen, die sich aus einem HTTP-Request erzeugen lassen. In der Implementierung für den Typ »User« (Listing 4) ließe sich etwa ein Login-Cookie auslesen und ein User aus einer Datenbank laden. Tritt bei einem Schritt ein Fehler auf (weil etwa ein gültiger Login-Cookie fehlt), gilt die Validierung für den Request Guard als gescheitert und Rocket ruft den Handler »balance()« gar nicht erst auf.

Request Guards helfen jedoch auch in zahlreichen anderen Situationen. Dabei zieht die Implementierung von »FromRequest« nicht automatisch eine Validierung nach sich, sondern es ist auch erlaubt, dass sie stets gelingt. So ließe sich zum Beispiel ein Typ »Language« erzeugen, der automatisch die Sprache des Nutzers aus »Accept-Language«-Headern und gesetzten Cookies ermittelt. So ist es dann für die einzelnen Handler extrem einfach, die Anzeigesprache zu ermitteln.

Responder und HTML

Bisher geben alle Handler einen simplen String zurück. Dies reicht zwar für kleine Beispiele aus, echte Webapplikationen sollen aber selbstverständlich richtige HTML-Antworten generieren. Natürlich ist die Idee naheliegend, einfach HTML in dem String zurückzugeben. Es existieren allerdings deutlich bessere Möglichkeiten.

Zunächst: Der Rückgabetyp eines Handlers in Rocket muss das Trait »Responder« [8] implementieren, das im Erfolgsfall ein »Ok(Response)« und im Fehlerfall ein »Err(Status)« zurückliefert. Für den Typ »String« gibt es bereits eine Implementierung, die einfach den String sendet und dabei den »Content-Type« Header automatisch auf »text/plain« setzt. Auch der Typ »NamedFile« implementiert das »Responder«-Trait: Dadurch streamt Rocket den Inhalt der Datei und setzt den »Content-Type« automatisch abhängig von der Datei-Endung.

Um nun HTML zu senden, braucht der Entwickler einen Typ, der ebenfalls »Responder« implementiert und den »Content-Type« auf »text/html« setzt. Hier bietet Rocket, wie viele andere Webframeworks, eine einfache Integration mit Hilfe so genannter Template-Engines an. Meist definieren Webentwickler den HTML-Code in einer Template-Datei, die Platzhalter enthält (wie zum Beispiel »<h1>{{ title }}</h1>«). Auch das ist mit Rocket möglich, doch gibt es eine interessantere Möglichkeit, die nur wenige andere Sprachen erlauben.

Die Template-Engine Maud [9] zeichnet sich dadurch aus, dass sie Templates direkt im Rust-Code in einer speziellen Syntax definiert (Listing 5). Das wichtigste Kennzeichen ist das Makro »html!«. Mit Makros definieren Entwickler beliebige DSLs (Domain Specific Languages), die Rust dann zur Kompilierzeit parst und verarbeitet. Auch Maud verfolgt dabei Rusts Hauptziele: Geschwindigkeit und Robustheit.

Listing 5

Mauds HTML-Templates

01 use maud::{DOCTYPE, html, Markup};
02
03 #[get("/primes")]
04 fn primes() -> Markup {
05     let primes = [2, 3, 5, 7, 11, 13];
06
07     html! {
08         (DOCTYPE)
09         html {
10             head {
11                 title "Primzahlen"
12             }
13             body style="background-color: green" {
14                 h1 { "Die ersten " (primes.len()) " Primzahlen:" }
15                 ul {
16                     @for p in &primes {
17                         li (p)
18                     }
19                 }
20             }
21         }
22     }
23 }

Weitere Features

Die gezeigten Beispiele vermitteln einen ersten Eindruck davon, wie Rocket funktioniert. Natürlich bietet das Framework noch mehr Funktionen, als diese einfachen Beispiele zeigen. Im Folgenden stellt der Artikel einige der noch nicht genannten Features kurz vor.

In den Pfaden der Routen greifen Entwickler nicht nur Segmente, sondern auch ganze Pfade ab. Die sind gegen Path Traversal Attacks gesichert, Aufrufe wie »server.com/../../../etc/passwd« funktionieren also nicht. Mit den wenigen Zeilen aus Listing 6 stellt Rocket so einen sicheren Dateiserver her.

Listing 6

Sicherer Dateiserver

01 #[get("/<p..>")]
02 fn files(p: PathBuf) -> Option<NamedFile> {
03     NamedFile::open(Path::new("static/")
04         .join(p)).ok()
05 }

Auch das Verarbeiten von »POST«-Anfragen mit Daten ist in Rocket sehr einfach (Listing 7). Hier behandelt der Handler »login« die »POST«-Anfragen mit dem Pfad »/login«, wobei Rocket automatisch versucht die gesendeten Daten als »LoginData« zu parsen. Der Responder »Redirect« antwortet automatisch mit einem 300er-HTTP-Statuscode und leitet den Nutzer zu einer anderen URL.

Listing 7

Verarbeitung von Formulardaten

01 #[derive(FromForm)]
02 struct LoginData {
03     username: String,
04     password: String,
05     remember_me: bool,
06 }
07
08 #[post("/login", data = "<login_data>")]
09 fn login(login_data: Form<LoginData>) -> Redirect { /* [...] */ }

Außerdem macht Rocket es mit dem »Json«-Responder einfach, Json-Antworten an den Browser des Benutzers zu schicken. Die so genannten Fairings ähneln dabei Middleware aus anderen Webframeworks. Sie erlauben es, beim Verarbeiten eines Request an gewissen Punkten einzugreifen, um beispielsweise Daten zu ändern oder für Statistiken zu sammeln.

Fairings führt Rocket für alle Requests unabhängig vom benutzten Handler aus. Schließlich bietet Rocket ein integriertes Testframework, welches das Testen der Webapplikation vereinfachen soll.

Offene Baustellen

Rocket nutzt Stärken von Rust und lebt dessen Philosophie: Typsicherheit und frühe Fehler-Erkennung für robuste Applikationen. Dies stellt Rocket in den Kontrast zu vielen altgedienten Webframeworks, die meist in dynamisch und schwach typisierten Sprachen wie Javascript, Ruby und PHP geschrieben sind. Auch wenn Typescript der Javascript-Community gerade vorführt, welche Vorteile ein Typsystem hat, dürfte dem durchschnittlichen Webentwickler die Art der Programmierung mit Rocket fremd vorkommen.

Obwohl robuste und fehlerfreie Webapplikationen selbstverständlich wünschenswert sind, hat diese Ausrichtung auch Nachteile, die gerade in der Webentwicklung besonders schmerzlich sein könnten: Da der Compiler Zeit braucht, dauert es mit Rust und Rocket immer ein wenig, bis Änderungen sichtbar werden. Bereits bei den im Artikel vorgestellten Beispielen liegt die Kompilierzeit zwischen einer und zwei Sekunden auf einem halbwegs modernen Notebook. Ist diese Zeit noch zu verschmerzen, kann die Verzögerung bei größeren Projekten merklich zunehmen. Dann können selbst fünf Sekunden den Entwickler unnötig behindern.

Watchdog aufsetzen

Das Buildtool Cargo erlaubt es, eigene Subcommands zu definieren, die der Entwickler einfach als ausführbare Dateien mit dem Namen »cargo-Mein Subkommando« im »$PATH« ablegt. Interessant für Rocket ist das externe Subcommand »cargo watch«, das bei Änderungen am Code diverse Prozesse anstoßen kann.

Für die Webentwicklung ist insbesondere der Befehl »cargo watch -x run« nützlich, der bei jeder Änderung im Code das Projekt neu kompiliert und den Server neu startet.

Die Rust-Community ist sich des Problems der Kompilierzeiten bewusst. Seit einiger Zeit arbeiten Compiler-Entwickler daher an einer inkrementellen Übersetzung (Incremental Compilation), die dabei helfen soll, nur die relevanten Teile des Codes neu zu kompilieren. Die ersten Ergebnisse dieser Technik sehen zwar vielversprechend aus, sind jedoch noch lange nicht perfekt.

Auch sollte an dieser Stelle erwähnt werden, dass Rocket noch einige Features fehlen, die routinierte Webentwickler von einem Webframework erwarten. Dazu gehört ein eingebauter Datenbank-Support. Zwar lässt sich Rocket auch heute schon zusammen mit Datenbanken verwenden (beispielsweise mit dem Query-Builder Diesel [10]), allerdings gestaltet sich diese Möglichkeit in der Praxis noch umständlicher, als sie sein müsste. Eine einfache Datenbank-Integration steht jedoch bereits für die nächste Rocket-Version auf dem Zettel, die vermutlich noch 2018 erscheinen soll.

Wer also ein ausgereiftes Webframework sucht, das alles Wichtige im Gepäck hat, wird mit Rocket derzeit wohl noch hadern. Auch Webentwickler, die nach Facebooks “Move fast and break things”-Philosophie leben, sollten vermutlich eher die Finger von Rocket lassen. Für Rust-Interessierte und Webentwickler, die neue Herausforderungen suchen, wäre Rocket aber womöglich eine interessante Spielwiese. (kki)

Der Autor

Lukas Kalbertodt studiert Informatik an der Universität Osnabrück, ist seit über drei Jahren in der Rust-Community aktiv und hat 2016 eine der ersten Vorlesungen zum Thema veröffentlicht: https://github.com/LukasKalbertodt

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