14. April 2022 von Christian Lunau
Rust Rocks
Von Zeit zu Zeit tauchen neue Programmiersprachen auf, die meist schnell wieder in der Bedeutungslosigkeit verschwinden. Selten wird eine neue Sprache auch von einer großen Community getragen und von globalen IT-Unternehmen für die Umsetzung ihrer eigenen Kernprojekte verwendet. Rust ist so eine seltene Sprache.
Was ist Rust?
Rust ist eine Multiplattform- und Multiparadigmen-Programmiersprache , die als Open Source veröffentlicht wurde und von einer großen Community aktiv weiterentwickelt wird. Die Sprache ist darauf ausgelegt, möglichst viele Performance- und Sicherheitsaspekte bereits zur Kompilierzeit zu berücksichtigen und nicht erst zur Laufzeit der generierten Anwendungen. Dies erspart beispielsweise aufwändige Speicherverwaltung und Aufräumarbeiten durch Garbage-Collector-Mechanismen, was sowohl der Laufzeitperformance als auch der Vorhersagbarkeit von Laufzeiten zugute kommt.
Aufgrund der Struktur von Rust sind die Kosten für Sprachabstraktionen so gering wie möglich, wodurch eine mit C/C++ vergleichbare Performance erreicht wird, ohne die notwendige Speichersicherheit zu vernachlässigen. So wird z.B. zur Compile-Zeit durch einen Borrow-Checking genannten Mechanismus sichergestellt, dass zu jedem Zeitpunkt (!) klar ist, welcher Variableninhalt aktuell gültig (und in-scope) ist. Stichwort Highlander-Prinzip: Es kann nur einen ("Owner" eines Speicherbereichs) geben. Alle anderen dürfen schauen (Speicherinhalt lesen), aber nicht anfassen (Speicherinhalt verändern). Verlässt der Owner den Raum (geht ‘out of scope’), geht die Ownerschaft verloren (der gehaltene Speicher wird automatisch freigegeben).Dass alles korrekt koordiniert wird, wird durch den Borrow-Checker zur Compile-Zeit sichergestellt. Ein im Hintergrund laufender Garbage-Collector zum Einsammeln der freien Speicherbereiche ist nicht erforderlich.
Kein permanent laufender Bereinigungsprozess im Hintergrund:
- keine zusätzliche Laufzeit
- dadurch weniger ausgeführte CPU-Befehle
- höhere Performance und geringerer Energieverbrauch.
Zudem wird Rust-Sourcecode nativ kompiliert, was die Grundlage für die hohe Performance ist: Zur Laufzeit wird nicht mehr interpretiert oder als Bytecode durch eine virtuelle Maschine verarbeitet. Rust-Programme sind wie Eisenoxid - nahe am “Bare Metal” der Maschine, es gibt nichts mehr dazwischen.
Wer steckt dahinter?
Ursprünglich wurde Rust von Mozilla entwickelt, bevor die Aufsicht über die Weiterentwicklung an die Ende 2020 neu gegründete Rust Foundation übergeben wurde.
Gründungsmitglieder der Foundation sind Microsoft, Huawei, Google, Amazon/AWS und Mozilla. Diese stellen auch das Board of Directors. Hinzu kommen Meta (Facebook) als Platin-Sponsor und weitere bekannte Namen wie arm, Dropbox, Threema und Toyota-Connected.
Aber fast am wichtigsten ist die extrem positive und hilfsbereite Community, die hinter Rust steht!
Einen guten Einstieg bietet zum Beispiel die Seite users.rust-lang.org.
…und was machen die mit Rust?
- Mozilla: Mozilla verwendet Rust in vielen seiner Hauptapplikationen, zum Beispiel Servo und Schlüsselkomponenten von Firefox
- Dropbox: Mehrere Komponenten des Dropbox Kern-Dateispeichersystems wurden in Rust neu geschrieben mit dem Ziel, eine höhere Effizienz im Datacenter zu erreichen. Es wird aktuell für jeglichen Dropbox-Speicher verwendet und bedient über 500 Millionen anwendende Personen.
- Amazon/AWS: Amazon/AWS hat kürzlich wichtige Teile der Video-App für Prime Video von JavaScript auf Rust + Webassembly umgestellt. Dabei seien in ersten Tests Geschwindigkeitssteigerungen um das 10- bis 25-fache erreicht worden. Die Bildwiederholrate nähert sich dadurch nun 60 FPS auf jedem der unterstützten Geräte.
- AWS hat Rust schon länger für performancekritische Dinge in Lambda, EC2 und S3 verwendet. Die Firecracker VMM ist komplett in Rust geschrieben.
- Google: Google hat sein experimentelles Betriebssystem Fuchsia zum Teil in Rust geschrieben, plant Rust als Systemsprache für Android einzuführen und untersucht Möglichkeiten, die Speichersicherheit in Chrome durch Verwendung von Rust zu verbessern.
- Microsoft: Microsoft hat mit Rust for Windows einen Rust crate veröffentlicht, der es ermöglichen soll, die komplette Windows-Api aus Rust heraus zu nutzen und bietet mittlerweile selbst eine Schulung zu Rust an. Siehe auch Microsoft: Why Rust for safe systems programming.
- Meta/Facebook: Meta/Facebook hat seinen Source Control Backend von Python nach Rust portiert, hier ein Video zu dem Thema von einer der Rust-Konferenzen.
Und sonst so?
Eine ganze Reihe von Firmen und Organisationen setzen mittlerweile Rust in ihren Projekten ein, hier eine Liste von production users. Eine weitere Aufstellung von Projekten ist von Sylvain Kerkour in Rust in Production 2021 veröffentlicht worden.
An dieser Stelle noch zwei Beispiele:
- Discord: Hier werden Rust und Elixir verwendet, um eine Skalierung auf 11 Million gleichzeitige User mittels Elixir NIFs (Native Implemented Functions) zu realisieren. Dabei ermöglicht Rust eine Beschleunigung der bestehenden Elixir codebase, wobei gleichzeitig die Speichersicherheit gewährleistet wird. Zusätzlich wurde der Read States Service in Rust neu geschrieben, der ursprünglich in Go implementiert worden ist. Während die Go-Version des Services zwar meist schnell genug war, kam es gelegentlich zu großen Latenzspitzen, die sich auf Go’s Speichermodell und den dabei verwendeten Garbage-Collector zurückführen ließen. Um dies zu beheben, hat Discord zu Rust gewechselt, das über ein einzigartiges memory allocation system verfügt, welches garbage-collection unnötig macht.
- Rust auf dem Weg in den Linux-Kernel: Es sind bereits einige (zunächst kleine) Teile im Linux-Kernel in Rust implementiert worden, Linus Torvalds steht dem vorsichtig-positiv gegenüber.
Das Rust-4-Linux-Team hat mittlerweile einen Großteil der Linux-C-API in Rust verfügbar gemacht, sodass dieser für in Rust geschriebene Kernel-Mode-Programme/Module verfügbar ist.
Worauf läuft’s?
Die Rust-Build-Umgebung selbst und mit Rust erzeugte Programme laufen auf allen relevanten Betriebssystem-CPU-Kombinationen.
Von den Rust-Maintaineren gibt es eine in drei Tiers abgestufte Zusicherung der Lauffähigkeit. Für die folgenden Plattformen ist die Lauffähigkeit garantiert (dies wird als ‘Tier 1’ bezeichnet):
- 64-bit MSVC (MS-Windows ab Version 7)
- 64-bit Linux (kernel 2.6.32+, glibc 2.11+)
- 64-bit macOS (10.7+, Lion+)
- 64-bit MinGW (Windows 7+)
- ARM64 Linux (kernel 4.2, glibc 2.17+)
- 32-bit MSVC (Windows 7+)
- 32-bit MinGW (Windows 7+)
- 32-bit Linux (kernel 2.6.32+, glibc 2.11+)
Zu den weiteren unterstützten Plattformen zählen Kombinationen aus einer Vielzahl von CPU-Varianten wie x86/i686/AMD64, ARM, MIPS, PowerPC, RISC-V, SPARC mit Betriebssystemen wie Android, iOS, Fuchsia, Solaris, Illumos, BSD und diversen Varianten von Linux sowie ein paar Exoten.
Tom Heimbrodt kommt 2019 in seiner Bachelorarbeit “Evaluierung der Sprache Rust zur Programmierung von Mikrocontrollern” für die Universität Magdeburg zu dem Schluss, dass die Hardwareunterstützung im Mikrocontroller-Bereich schon gut, aber ausbaufähig ist. Er untersucht hier neben der generellen Lauffähigkeit vordringlich Performance und Speicherverbrauch, die oft in der Nähe der zum Vergleich herangezogenen Implementierungen als C-Applikationen liegen.
Durch die Unterstützung von WebAssembly/WASM rückt Rust in die Nähe von JavaScript-Entwicklungen und ermöglicht die Realisierung von höher Performanten Applikationen auch für Web-Umgebungen. Elisabeth Schulz hat das in ihrem im Juni 2021 auf Informatik Aktuell veröffentlichten Artikel “Serverless und doch Metal: AWS Lambda mit Rust” näher beleuchtet.
Mit Hilfe von CI-Pipelines in GitHub-Actions, TravisCI, AppVeyor oder anderen lassen sich Builds, Tests und Deployment auch für unterschiedliche Plattformen parallel durchführen.
Was macht Rust besonders?
- Memory safety: Zur Compile-Zeit durchgesetzt, zur Laufzeit ohne Performanceverluste vorhanden.
- Performance: Nahe beim erreichbaren Maximum.
- Energieeffizienz: Durch die sparsame Nutzung der Computerresourcen (damit ist geringer CPU-Anspruch und moderater RAM-Speicherverbrauch gemeint) wird für die Ausführung von Rustprogrammen verhältnismäßig wenig Energie benötigt.
- Multiplattform: von Anfang an nicht nur portabel, sondern portiert
- Fehlermeldungen: Fehlermeldungen zur Compile-Zeit, die nicht nur Sinn ergeben, sondern hilfreich bis lehrreich sind.
- Cargo: Integriertes Standard-Build-Tool und Repository-Integration.
- Dependencies: Dependencies (zu Versionen von crates (Libraries)) werden ebenfalls von cargo gehandhabt. Durch standardisierte und einfach steuerbare Dependencies kann auf entdeckte Security Vulnerabilities in Open Source Libraries schnell und leicht reagiert werden.
- cargo clippy: cargo clippy bietet ausgefeiltes static linting
- cargo doc: Automatisierte Dokumentationserstellung mittels ‘Inline-comment to html-doc’ im Standard mitgeliefert
Die Kombination von geringem Energieverbrauch, hoher Performance und gleichzeitig hoher Speichersicherheit macht Rust einzigartig.
Insgesamt bietet Rust schon mit dem mitgelieferten Build-Tool cargo eine umfassende und moderne Programmierumgebung, deren Möglichkeiten bei anderen Programmiersprachen erst durch Ergänzung weiterer, externer Tools annähernd erreicht werden könnte. Mit dem Plugin RustAnalyzer existiert zudem eine hervorragende Einbindung in die Visual Studio Code IDE.
Energieeffizienz
Heute eingesetzte Programme laufen oft und viel auf Geräten, bei denen der Energiebedarf direkt mit der Nutzbarkeitsdauer des Gerätes gekoppelt ist. Smartphones zum Beispiel bedanken sich mit längerer Akkulaufzeit für höherperformante Apps. Erst recht gilt das, je kleiner die Geräte werden, etwa im IOT-Bereich.
Aber auch am anderen Ende der Skala ist Energieverbrauch von Software durchaus relevant: In großen Rechenzentren ist die erzeugte und abzuführende Temperatur direkt gekoppelt mit der Last, die auf den CPUs der Maschinen liegt. Mit effizienterer Software kann nun entweder mit der selben Last mehr getan werden (mehr Kundinnen und Kunden bedient werden) oder die Dimensionierung der Maschinen kann für dieselbe Leistung kleiner ausfallen und somit beispielsweise kostbaren Strom sparen.
In Zeiten, in denen wir aktiv planen, die schon sichtbaren Auswirkungen des Klimawandels zu verringern, ist Energieeffizienz sehr relevant. Dass durch höhere Performance der gelieferten Applikationen die User glücklicher werden können ist dabei ein weiterer schöner Effekt.
Durchaus vorstellbar ist zudem, dass schon bald auf Nachhaltigkeit bedachte europäische Regierungen die Energieeffizienz von industriellen Erzeugnissen (also auch Software) einfordern werden. Vielleicht in Form einer genaueren Ausarbeitung der Energieeffizienzrichtlinie des Europaparlaments im Rahmen der Green-IT Initiative.
Die Effizienz der Erstellung von Software, also die Zeit, die wir benötigen um sie korrekt lauffähig, sicher und schön zu machen, ist im Vergleich zur später anfallenden Betriebslaufzeit verschwindend gering. Das gilt insbesondere dann, wenn man die Zeit mit der Anzahl der Installationen/Geräte multipliziert.
Es muss also völlig egal sein, ob zur Software-Erstellung jetzt Zeit J oder eine geringfügig längere Zeit R benötigt wird, wenn durch verbesserte Energieeffizienz und Performance dann während der Betriebszeit dauerhaft weniger Energie verbraucht wird. Da fertige Rust-Applikationen zudem noch mit hoher Wahrscheinlichkeit durch weniger Laufzeitprobleme (weil beispielsweise Speicherfehler vermieden werden) und angenehmere User-Experience (durch kürzere Response-Zeiten) die Menschen erfreuen, ist erst recht klar, dass wir alle davon profitieren.
Betrachtet man als Entwicklungszeit nicht die Zeit bis zur Auslieferung der ersten Version eines Produktes, sondern bis es nachprüfbar stabil und verlässlich auch ‘im Feld’ läuft, dann kann erwartet werden, dass diese Fertigstellungszeit in Rust eher kürzer ist als bei Projekten in solchen Programmiersprachen, die ihren Reifegrad erst zur Anwendungszeit entfalten.
Studie zur Energieeffizienz von Programmiersprachen
Vor einiger Zeit hat eine Forschergruppe in Portugal näher untersucht, wie es mit Ausführungsgeschwindigkeit, Speicherverbrauch, Startzeiten, Kompilierdauer und weiteren Details bei verschiedenen Programmiersprachen aussieht und im Mai 2021 dazu einen vielbeachteten Artikel veröffentlicht. Eine ältere Vorversion dieser Arbeit kann man sich hier ansehen.
Dabei wurden für alle beteiligten Sprachen jeweils möglichst effiziente Implementierungen für dieselben Algorithmen gewählt und diese auf derselben Hardware und in derselben Betriebssystemumgebung ausgeführt.
Ende Mai 2021 hat Christian Meesters diese Ergebnisse noch mal aufbereitet und die Ergebnisse im folgenden Diagramm übersichtlich dargestellt:
Das Diagramm ist freundlicherweise unter der sehr freigiebigen CC-BY SA 4.0-Lizenz veröffentlicht.
Zu sehen ist hier jeweils der Energieverbrauch des Test-Sets an Implementierungen, relativ normiert zu der energiesparendsten Sprache, welche C ist und die den Wert ‘1’ erhalten hat. Die anderen Sprachen benötigen für dieselben Aufgaben dementsprechend die Y-fache Energie.
Schaut man sich die Ergebnisse im Detail an, dann ergibt sich aus der Originalstudie, dass für diese Berechnungsaufgaben Rust etwa 3,5 Prozent mehr Energie benötigt als C und beide auch fast gleich schnell sind. Java-Applikationen benötigen ungefähr doppelt so viel Energie und Zeit wie Rust-Applikationen. Insbesondere benötigen Lösungen in VM-basierten Sprachen und erst recht Skript-Sprachen generell ein Vielfaches der Energie und Zeit von compilerbasierten Sprachen.
Je nach tatsächlich getestetem Algorithmus weichen in der Studie die Werte ein wenig ab, bleiben in ihrer Relation zueinander aber im Wesentlichen gleich angeordnet.
Betrachtet man statt der zum Energieverbrauch analogen Performance die bei der Ausführung benötigten RAM-Mengen, so ist das Ergebnis ähnlich:
Rust benötigt hier zwar 54 Prozent mehr RAM (Faktor 1,54) als die speichersparsamste Programmiersprache, welche in diesem Vergleich Pascal ist. Der Speicherbedarf der Pascal-Programme liegt sehr knapp unter denen in Go und C. Im Vergleich zum Rust-Speicherbedarf benötigt aber zum Beispiel JavaScript das 4,56-fache, Java das 6,01-fache und das Schlusslicht im Speicherverbrauch, Jruby das 19,84-fache an RAM.
Kritisch anzumerken ist, dass nicht jede Sprache für dieselben Zwecke gleich gut geeignet ist. So eignen sich Skriptsprachen gut für Datenstromverarbeitungen auf Backend-Rechnern, die ‘auf die Schnelle’ implementiert werden sollen, während kompilierte Anwendungen eher für Anwendungsfälle geeignet sind, bei denen es auf maximalen Datendurchsatz oder zuverlässige und schnelle Antwortzeiten ankommt.
Leider besteht ein Zusammenhang zwischen der beobachteten Energieeffizienz einer Sprache und ihrer Erlernbarkeit:
Gerade die effizienten Sprachen sind eher schwerer zu erlernen. Während man mit Perl schnell etwas Lauffähiges hinkritzeln kann und auch in Java recht schnell Programme erzeugt werden können, die so aussehen, als würden sie nicht abstürzen, ist das bei Rust anders: Beim Kompiliervorgang steht hier zum Beispiel der Speicherwächter (Borrow-Checker) vor der Tür und spricht ein deutliches “You shall not pass!”, bis auch wirklich alle Referenzen und Speichergültigkeiten (Scopes, Lifetimes) korrekt behandelt werden. Danach hat man aber auch eine gewisse Sicherheit, dass das erzeugte Compilat den Speicher korrekt behandelt.
Security
Gegen explizit programmierte Features in einer Applikation oder Library kann auch eine noch so gute Absicherung in der verwendeten Programmiersprache nichts ausrichten.
Hätte es im Rust-Ökosystem ein vergleichbar programmiertes Library-Feature gegeben, wäre auch hier ‘die Kacke am Dampfen’ gewesen.
Schaut man sich an, wie die Rust-Community, in diesem Fall insbesondere die Language- und CrateMaintainer, mit anderen Sicherheitslücken umgegangen sind, so findet man folgendes:
Ende 2021 gab es einen Bug im Unicode-Handling des Rust-Compilers, der potenziell dazu hätte genutzt werden können, im Sourcecode verschleiert ineffektive Zugangsprüfungen einzuschleusen. Dabei sieht der Sourcecode für einen menschlichen Leser zunächst ok aus, der Compiler ersetzt dann aber die Prüfung einer Passwortvariablen mit einem konstanten Wert.
Das ist nicht ganz so eine Katastrophe wie der Log4Shell-Bug, aber definitiv unschön.
- 25.07.2021: Die Vulnerability wurde an das zuständige Rust-Team gemeldet, welches mit der Arbeit an einer Lösung begann.
- 14.09.2021: Dem Team wird der Termin 01.11.2021 für die Presseveröffentlichung des Problems mitgeteilt.
- Bis 17.10.2021 sind dann ALLE jemals auf dem zentralen Repository für Rust Libraries (crates.io) veröffentlichen Sourcen auf diese Angreifbarkeit hin analysiert worden. Insgesamt wurden dabei 5 Sourcen gefunden, die eine dem Angriffszenario ähnelnde Unicode-Prüfung enthielten, keine davon war tatsächlich bösartig.
- 01.11.2021: Das Presse-Embargo wird aufgehoben, die Angreifbarkeit veröffentlicht und das gefixte Rust 1.56.1 herausgegeben.
Nun ist die Angreifbarkeit hier anders gelagert als bei Log4J: Während es bei der Java Logging Library ausreicht, einen bösartigen Text durch eine beliebige Fremdsoftware protokollieren zu lassen, muss für die Unicode-Prüfung bereits Quellcode in ein Endprodukt eingeschleust werden. Theoretisch ist dies über Open-Source-Bibliotheken möglich, die im Falle von Rust allerdings fast ausschließlich über das crates.io-Repository angeboten werden. Wie das Rust-Team gezeigt hat, ist es durchaus möglich, hier nach Sicherheitslücken zu suchen und diese gegebenenfalls auch rechtzeitig zu beheben.
Aber das ist erst die halbe Miete. Wie bekommen die Nutzerinnen und Nutzer nun die bereinigte Version auf ihren Rechner? Hier wird es etwas komplizierter. Im Gegensatz zu vielen gängigen Programmiersprachen werden Anwendungen in Rust normalerweise statisch gelinkt. Das bedeutet einerseits, dass man der DLL/Shared-Library-Hölle entkommt (und einige andere Vorteile hat), andererseits aber auch, dass eine anfällige Bibliothek nicht wie bei Log4J mal eben von den Betreiberinnen und Betreibern und/oder Anwenderinnen und Anwendern ausgetauscht werden kann. Denn alles steckt in der fertig gelinkten Anwendung. Hier muss also zunächst der Anbieterinnen und Anbieter und/oder Entwicklerinnen und Entwickler aktiv werden und eine neue Version des Executables erstellen. Am besten bevor die Schwachstelle der verwendeten Bibliothek veröffentlicht wird.
Einerseits können Betreiberinnen und Betreibern und/oder Anwenderinnen und Anwendern bei Rust-Applikationen keine Sicherheitslücke durch Austauschen einer externen Bibliothek fixen, andererseits muss man das bei Rust auch nicht: Die Pflege der Anwendungsbinaries selbst obliegt den Anbietenden, also zum Beispiel uns - machen wir dann doch gerne.
Anekdote: Der Your-Code-Is-Inefficient-Error
Der folgende Screenshot zeigt eine Konsolenausgabe, wie sie der Rust-Compiler typischerweise im Fehlerfall ausgibt. Zu sehen ist ein Codeausschnitt mit Zeilennummern und genauen Angaben, wo welcher Fehler auftritt, oft auch angereichert mit Hinweisen zur Fehlerbehebung und Verbesserung des Codesegments:
Bitte genau hinschauen: der Compiler bemängelt hier, dass der Entwickler offensichtlich etwas sortieren will, dafür aber den bekanntermaßen ineffizienten Algorithmus ‘Bubblesort’ codiert hat. Der Compiler lehnt dies mit einer Fehlermeldung ab.
OK, das ist ein Hoax, der in der Rust-Community entstanden ist, so weit geht der Compiler dann doch nicht; aber dass dieser Hoax in der Community so ausgebrütet wurde und der Screenshot nach seiner Veröffentlichung viral ging, zeigt auch, welchen Stellenwert hilfreiche Fehlermeldungen in der Rust-Community haben. Tatsächlich sehen viele Fehlermeldungen ähnlich aus, sind präzise und enthalten ähnlich hilfreiche Informationen. In gewissem Maße kann man so durch Versuch und Irrtum lernen, in Rust zu programmieren. Das geht mit Rust besser als mit wahrscheinlich jeder anderen Sprache.
Warum ist Rust für adesso interessant?
- Rust ist nachhaltig und energieeffizient.
- Rust bietet eine hohe Performance und spart Zeit beim User.
- Mit Rust wird die Stabilität von Applikationen frühzeitiger erreicht.
- Die technische Qualität der Applikation steigt.
- Mit Rust kann die Gesamtzeit, bis ein stabiles Produkt entstanden ist, verringert werden.
Fazit
Die Zeit, die die vielen Anwender damit verbringen, auf die Ergebnisse von Softwareberechnungen zu warten oder sich durch umständliche Anwendungen zu quälen, ist nicht zuletzt auch Lebenszeit, die jeder sicherlich sinnvoller als mit Warten verbringen möchte.
Gerade in Zeiten, in denen die verantwortungsbewusste Welt das Klima retten will, ist Energieeffizienz ein wesentliches Qualitätsmerkmal von Softwareprodukten.
Da Rusts Speichersicherheitsmethodik gerade bei langlaufenden Anwendungen zu erhöhter Stabilität und auch verlässlicherem Antwortzeitverhalten führt, ist hier ein verbessertes Verhalten der Software im Betrieb zu erwarten.
Zusammen mit einem guten Tool-Ökosystem und dem freundlich-hilfreichen Compiler-Verhalten ergibt sich ein Gesamtpaket, das gerade für die Implementierung von ernsthaften Anwendungen unübertroffen ist.