Mit Kotlin und Go und der impliziten Nutzung von TypeScript haben wir bereits eine Vielzahl von modernen Programmiersprachen im Einsatz. Mit Kotlin haben wir eine Sprache, die den JVM-Stack mit syntaktischen Zucker perfekt ergänzt und die Codebase sinnvoll schlanker macht. Dazu haben wir bereits letztes Jahr eine kurze Einführung geschrieben. Auch zu Go gibt es eine Einführung die wir aufgesetzt haben als wir mit Go-Entwicklung intern angefangen haben. Es gibt mittlerweile zahlreiche Applikation, die wir komplett in Go umgesetzt haben und sind froh, dass wir uns Go als Skill einverleiben konnten, da die Vorteile bei der Entwicklung offensichtlich sind. Mit dieser Euphorie möchten wir auch Rust als Programmiersprache besser verstehen und abwägen ob und zu was sich diese relativ neue Sprache am besten eignet würde.
Ausgangspunkt unserer Anstrengung Rust auszuwerten ist die konsekutive Top-Platzierung im berühmten alljährlich veröffentlichen StackOver Developer Survey. Dabei belegt Rust seit 2016 den ersten Platz bezüglich Most Loved Programming Language. Dies können wir nicht einfach dem Zufall überlassen und haben uns dadurch entschieden Rust unter die Lupe zu nehmen und hoffen dass wir dadurch auch Ihnen einen Mehrwert in Bezug auf Qualität und Nachhaltigkeit geben können.
Ursprünge von Rust
Rust wurde ursprünglich durch den Mozilla-Mitarbeiter Graydon Hoare als persönliches Projekt entwickelt. Mitgewirkt hat dabei auch der bekannte Co-Founder von Mozilla, Erfinder von JavaScript und heutiger CEO von Brave Software, Brendan Eich. Rust fand seither intensiven Einsatz in Mozilla-Produkte. Einflüsse sind neben der Allzwecksprache C++ ganz klar Haskell und Erlang, zwei funktionale Programmiersprachen, die in der Kombination mit einer Maschinennahe-Sprache wie C++ ein durchaus interessante Kombination eingehen.
Mittlerweile nutzen viele Betriebe Rust als Systems Programming-Programmiersprache, wo Speicherallokation und Security eine grosse Rolle spielen. Es gibt ein komplettes Unix-like Betriebssystem Redox, welches in Rust geschrieben ist und auch Microsoft nutzt Rust um neue Windows-Komponenten zu schreiben. Zudem gibt es bekannte Big-Tech Player die Rust in ihrem Stack dazuzählen, darunter nennenswert ist Dropbox, Figma, Discord, 1Password und nicht zuletzt Facebook.
Eine durch die COVID19-Pandemie hervorgerufene Umstrukturierung zwang Mozilla die Entwicklung von Rust und dessen Ökosystem in eine separate Stiftung zu geben. Somit gibt es seit Februar 2021 eine dedizierte Rust Foundation, welche durch AWS, Huawei, Google, Microsoft und weiterhin Mozilla finanziell unterstützt wird um dessen Weiterentwicklung und Fortbestand garantieren zu können.
Einsatzgebiete für Rust sind als eine General Purpose-Programmiersprache laut der hauseigenen Doku breit gefächert und beinhalten unter anderem Command-Line Tools, Web-Services, DevOps-Tooling, eingebettete Systeme, Audio- und Video-Analysis, sowie Transcoding, Cryptowährungen, Bioinformatik, Suchmaschinen, Internet-of-Things Applikationen, Machine Learning, und nicht zuletzt grosse Teile des Firefox Web-Browsers. Beeindruckend wie sich eine relativ neue Sprache wie Rust bereits in verschiedensten Branchen etablieren konnte. Dies möchten wir nun ein wenig mehr auf den Grund gehen und schauen was Rust denn genau auszeichnet.
Merkmale von Rust
Rust ist wie eingangs erwähnt eine Allzwecksprache und eignet sich für alle Art von Systemprogrammierung. Hauptmerkmale legt dabei Rust auf die Ausgewogenheit von Performance und Systemstabilität. Dabei spielt der Compiler eine ganz zentrale Rolle, da er, nicht ganz ähnlich wie bei Go, opinionated, also weiss was er akzeptiert und was nicht, ist. Üblicherweise prüfen Compiler beim Bauen einer Codebase ob sich der Code auf Grund von Syntax und Typen (bei typisierten Sprachen) valide sind und überlässt eine ganze Klasse von potenziellen Fehlern der Laufzeit.
Bei konventionelle Sprachen wie Java, C++ oder auch Go, läuft zur Laufzeit hingegen neben der eigentlichen Applikationslogik auch einen Garbage Collector, welcher automatisch Memory Management betreibt und in einfachen Worten gehalten obsolet gewordene Speichereinheiten zur Wiederverwendung frei gibt. Rust verfügt über keinen Garbage Collection und prüft zur Compilezeit mit einer sogenannten Escape-Analysis ob der Code zu keiner Speicher-induzierten Fehlern führen könnte. Das heisst, das Rust durch das Weglassen des Garbage Collector schlankere und perfomantere Artefakte bauen kann. Rust bietet als Kompensation für das Garbage Collecting ein Ownership-Verständnis, welcher sich ein angehender Rust-Programmierer zuerst aneignen muss. Wir erklären dieses Ownership-Konzept zu einem späteren Zeitpunkt in diesem Beitrag.
Bevor wir weiter auf die Eigenschaften von Rust eingehen, hören wir uns zuerst einmal an wie die offizielle Rust-Dokumentation ihre eigene Sprache –am besten, wie ich denke– zusammenfasst:
Rust is for people who crave speed and stability in a language. By speed, we mean the speed of the programs that you can create with Rust and the speed at which Rust lets you write them. The Rust compiler’s checks ensure stability through feature additions and refactoring. This is in contrast to the brittle legacy code in languages without these checks, which developers are often afraid to modify. By striving for zero-cost abstractions, higher-level features that compile to lower-level code as fast as code written manually, Rust endeavors to make safe code be fast code as well.
The Rust language hopes to support many other users as well; those mentioned here are merely some of the biggest stakeholders. Overall, Rust’s greatest ambition is to eliminate the trade-offs that programmers have accepted for decades by providing safety and productivity, speed and ergonomics.
The Rust Programming Language Documentation, Introduction [Quelle]
Man merkt, dass bei der Entwicklung von Rust das Problem im Vordergrund stand, wie man gewünschten Programmier-Komfort in Form von Abstraktions- und High-Level-Eigenschaften mit einer Fokussierung auf Maschinen-nahe Performanz erzielen kann; die sogenannten zero-cost abstractions.
Um die Features und Benefits von Rust nochmals kurz zusammenzufassen, kann man folgende Liste berücksichtigen:
-
Konsistente Speichersicherheit garantiert durch den opinionated Rust-Compiler
-
Explizite Nebenläufigkeit und Parallelität durch Data Ownership Model
-
Abstraktion und Entwicklungskomfort zum Nulltarif (heisst keine Performance-Einbussen)
Nun, jetzt haben grob beschrieben was Rust auszeichnet. Für Techies, die sich noch nie mit Maschinennahe Konzepten auseinandersetzen mussten, sind vielleicht das eine oder andere Konzept komplett neu und wissen wahrscheinlich im ersten Schritt noch nicht so genau was damit anzufangen ist. Aus diesem Grund sollten wir uns zu allererst das wichtigste bei Rust anschauen: das Data Ownership Model. Hoffentlich wird dadurch alles ein wenig verständlicher.
Ownership, Borrow Checking und Escape Analysis
Wie bereits angekündigt ist das grösste Merkmal bei Rust die Absenz von Garbage Collecting und das damit verbundene Konzept von Ownership, Englisch für Besitz oder Eigentum, aber in diesem Kontext eher am besten übersetzt mit Inanspruchnahme. Das Konzept von Ownership definiert den Lebenszyklus von Daten zur Laufzeit eines Rust-Programms.
Die Rust-Dokumentation definiert für das Konzept von Ownership 3 Regeln. Diese Regeln werden durch den Borrow Checker, ein Feature des Rust-Compilers, bei der Buildtime geprüft um sicherzustellen, dass der Lebenszyklus einer Dateneinheit valide ist. Der Borrow Checker nutzt hierfür ein im Compilerbau bekanntes Pattern mit dem Namen Escape Analysis. Escape Analysis verfolgt die Laufzeit einer Variable bis sie aus dem Geltungsbereich (Scope) fällt und entscheidet anhand davon was mit dem freigewordenen Speicherallokation gemacht werden muss. JVM-Sprachen wie Java oder allgemein Garbage-Collected Sprachen machen dies zur Laufzeit über dynamische Allokation von Variablen. Zurück zu den 3 Regeln. Die Regeln sind wie folgt (freilich aus dem Englischen übersetzte):
1. Each value in Rust has a variable that’s called its owner.
Jeder Wert hat in Rust eine Variable, und diese Variable ist dessen Besitzer.
2. There can only be one owner at a time.
Es kann stets immer nur einen Besitzer eines Wertes zu einem gegebenen Zeitpunkt geben.
3. When the owner goes out of scope, the value will be dropped.
Wenn der Besitzer den Geltungsbereich (Scope) des Wertes verlässt, wird der Wert getilgt.
Es ist sinnvoll kurz den Unterschied von Heap und Stack aus der Maschinennahen Speicherverwaltung in Erinnerung zu rufen. Mit Heap bezeichnet man dynamischer Speicher, welcher zur Laufzeit zusammenhängende Speicherabschnitte wieder beliebig freigeben kann, sobald diese zur Wiederverwertung gekennzeichnet werden. Mit Stack hingegen ist der statische Stapelspeicher der nach dem Last-In-First-Out-Prinzip, kurz LIFO, Datenwerte aufnimmt und wieder abgibt. Zur Laufzeit werden typischerweise Datenwerte in der Heap aufgenommen wenn dessen Lebenszyklus, sprich die potenzielle Mutabilität (verändert sich der Wert oder nicht) nicht bereits im voraus bekannt ist. Genauer gesagt prüft ein Compiler wie bei C++ zur Buildtime den Scope einer Variable und definiert dabei, ob dieser Wert eine kurze, im voraus bekannte Lebensdauer hat oder nicht, und anhand davon ob der Wert in die Heap oder in den Stack aufgenommen wird.
Hier haben wir ein Beispiel (geborgt von dev-notes.eu), der das in Rust-Code veranschaulicht.
|
|
Output:
|
|
Somit kann man sagen, dass es stets zwei Arten von Referenzierung von Werten geben kann:
-
Geteilte (shared) Referenzen mit
&
-
Mutable (variable) Referenzen mit
&mut
Des weiteren kann eine Referenz zu einem Wert nie länger existieren als der Wert, der referenziert wird. Die Idee von Ownership geht bei Rust noch ein bisschen weiter und macht die ersten Schritte in der Sprache ein wenig aufwändiger, da man das Management von Datenlebenszyklen wahrscheinlich in anderen, bekannteren Sprachen wie Java oder Python abstrahiert ist und nicht Teil der Aufgaben bei der Programmierung sind. Man muss hier einfach verstehen, dass Rust damit die Möglichkeit schafft korrekten Code zu schreiben, welcher mit einem Hauptmerkmal auf Security und Safety legt, was dessen Artefakte –vorausgesetzt man weiss was man tut– unglaublich robust und stabil macht. Wenn Sie mehr über Ownership und der Umgang mit Lebenszyklen von Daten bei Rust kennenlernen möchten, kann ich die offizielle Dokumentation empfehlen:
https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html
Installation mit RustUp
Um Rust zu installieren gibt es eine Toolchain, genannt rustup. Bei einer Unix-artigen Umgebung wie GNU/Linux oder macOS kann man direkt über folgenden Command rustup runterziehen und den Installationsprozess damit veranlassen:
|
|
ℹ️ Bei Windows-Maschinen genügt es das rustup-init.exe
downloaden und ausführen. Mehr Information dazu unter https://forge.rust-lang.org/infra/other-installation-methods.html
Das ausgeführte Shell-Skript installiert neben rustup, der eigentlichen Toolchain-Verwaltung, auch folgende Elemente:
-
rustc: Der eigentliche Rust-Compiler welcher aus Rust-Sourcecode die Binaries generiert.
-
rustfmt: Ein Format-Binary welche, ähnlich wie go fmt den Sourcecode automatisch entsprechende formattiert.
-
cargo: Cargo ist das Build-System und Package-Manager von Rust. Damit werden wir unsere Rust-Projekte bauen und eventuelle Dependencies auflösen.
-
std: Zuletzt werden noch die Standard-Libs der Sprache mitinstalliert.
Hello b-nova!
Eine ‘Hello b-nova!’-Funktion ist in Rust relativ schnell geschrieben. Man kann für die ersten Gehversuche ohne gleich Rust auf der lokalen Maschine installieren zu müssen in der offiziellen Rust Playground vornehmen.
|
|
Dazu einfach die Playground öffnen, Code-Snippet reinkopieren und Run klicken! Man sieht schön den Output des Build-Prozess, sowie den eigentlichen Output bei dessen Ausführung.
|
|
Bei Interesse kann man gleich das offizielle Tutorial in der Playground vornhemen, das sogenannte Tour of Rust. Dieses ist gleich aufgebaut und geht über die ganze Sprach-Spezifikation durch und lernt dabei wie man mit Rust if/else- und for-Schleifen schreibt, was die einzelnen Datentypen sind, usw. Schaut vielleicht gleich am besten in die Table of Content des Tutorials rein, welches nebenbei bemerkt komplett auf Deutsch übersetzt ist.
Bauen mit Cargo
Sobald wir ein richtiges Rust-Projekt aufsetzen, ist Cargo ein wichtiges Element im kompletten Lebenszyklus der zu bauenden Applikation. Stellt hierbei sicher, dass Cargo korrekt installiert ist und die Version von Rust auf der neusten Version ist.
|
|
Mit $ cargo new hello-bnova
wird Cargo mitgeteilt dass wir ein Projekt mit dem Namen hello-bnova haben möchte. Cargo übernimmt und stellt uns ein komplettes Verzeichnis zur Verfügung.
|
|
Cargo erstellt nicht nur das Verzeichnis, sondern gibt bereits eine kanonische Projektstruktur vor, welche neben .git
und einer .gitignore
auch den src/
-Folder, welche eine main.rs
beinhaltet, bestimmt, sowie Cargo.toml
und Cargo.lock
Dateien. Sehr nützlich wenn Sie mich fragen.
|
|
Bevor wir die main.rs
aufmachen, werden wir kurz über die Cargo.toml
drüber schauen und erklären was es damit auf sich hat.
Cargo.toml
|
|
Wie schon vermutet handelt es sich bei dieser toml-Datei um eine Meta-Datei der Rust-Applikation. Anhand dieser Datei löst Cargo anfällige Dependencies auf und weiss wie mit die Applikation beschaffen ist. Hier werden wir später eine Dependency pflegen und schauen wie Rust das macht.
Die src/main.rs beinhaltet das gleiche Snippet welches wir vorhin auf dem Playground ausgeführt hatten.
main.rs
|
|
Mit der Cargo-CLI können wir jetzt das soeben erstellte Projekt bauen. Normalerweise geht das mit cargo build, aber wir können das Kompilieren sowie die Ausführung dessen Artefakt in einer einzigen Zeile ausführen lassen.
|
|
Jetzt möchten wir ein wenig damit rumspielen und werden das beliebte CLI-Framework Clap einsetzen. Fügen wir zuallererst die Dependency https://crates.io/crates/clap hinzu. Diese liegt im Moment des TechUps in der Version 2.33.3
vor.
Cargo.toml
|
|
In der main.rs
bauen wir gleich das Grundgerüst von Clap mit ein.
main.rs
|
|
Lassen wir das Projekt nochmals mit cargo build
bauen.
|
|
Jetzt sollten wir in unserem target/
-Verzeichnis ein Binary hello-bnova
haben, welches wir als CLI-App ausführen können.
|
|
Das Grundgerüst von Clap macht noch ein wenig mehr als nur ‘Hello bnova!’ zu printen, aber grundsätzlich funktioniert die CLI-App bereits. Glückwunsch ! 🍀
Auch erwähnenswert sind die unglaublich expressiven und verständliche Fehlermeldungen bei der Kompilation. Ich habe versuchsweise “ durch ’ in einer String-Expression ersetzt. Der Kompilier zeigt mir nicht die Linie, sondern schlägt mehrere Lösungswege vor, welche ich oft einfach aus der Fehlermeldung kopieren kann.
|
|
Der Footprint einer CLI-App mit Rust beträgt in diesem Beispiel 3.9 MB, sicher die Hälfte schlanker als ein Gegenstück in Go (mit dem CLI-Framework Cobra).
|
|
Noch ein paar Worte zu Cargo und Crates
Das Ökosystem von Rust scheint nach gut zehn Jahren Existenz ausgereift und bietet Stand heute Libraries für alle möglichen Use-Cases. Der haus-eigene Package Manager Cargo hat ein eignene Crate-Registry, namentlich https://crates.io/. Darauf findet man alle vorhandenen Crates, die man in einem Rust-Projekt einbauen kann.
An diesem Punkt möchten wir kurz einen kleinen Überblick mit welcher Crate welchen Use-Case abdecken könnte. Eigentlich jeder der unten stehenden Einträge eine eigenes TechUp, da diese in ihrer jeweiligen Kompetenz umfangreich sind. Aber dennoch fassen wir diese hier kurz zusammen.
-
Clap: Clap ist ein CLI-Framework womit man, genau wie mit Cobra bei Go, eine CLI-App programmieren kann.
-
Rayon: Mit Rayon bringt man Parallelität und Nebenläufigkeit in ein Projekt ein.
-
Tokio: Mit Tokio baut man Event-driven, hochverfügbare, asynchrone Applikationen. Tokio ist dabei spezielle auf Leichtgewichtigkeit ausgelegt.
-
Rocket: Rocket ist das to-go Web-Framework für Rust und bietet gewohnte Features, die man von einem Web-Framework erwarten kann.
-
WASM: wasm-pack ist ein WebAssembly-BuildTool, welches aus Rust wasm-Artefakte generieren kann. Seit der Einführung von WebAssembly in 2019 unterstützt jeder Web-Browser die Möglichkeit wasm-Maschinencode auszuführen anstatt eine ECMAScript-kompatible Sprache für Client-seitige Applikationen zu nutzen. Dabei ist Rust, neben Kotlin und Go eine der wenigen Sprachen die bis dato WASM untersützen. Rust ist sich seither als Standard für WASM-Applikationen etabliert.
Fazit
Rust ist eine moderne Sprache, die Dinge etwas anders angeht und sich dabei als optimalen Kandidat für performante und stabile Systementwicklung eignet. Auch wenn vielen Rust noch kein Begriff sein wird und dessen Nutzung sich noch auf marginale Nischenbereiche zu konzentrieren scheint, so hat Rust interessante Eigenschaften, die Rust zur Sprache der Wahl in einem breiten Spektrum von Anwendungsmöglichkeiten macht, dabei nicht zuletzt robuste Web-Services und CLI-Apps.
Wir bei b-nova sind darauf spezialisiert neue Technologien auf Ihre Anwendbarkeit und Nachhaltigkeit zu prüfen und diese, falls unseren Aussichten auf Qualtitäszuwachs entsprechend, auch zum Einsatz und Erfolg von Kundenprojekten zu überführen. Wir sind sicher, dass sich Rust als ein weiteres wertvolles Werkzeug in unserer Toolbox etablieren wird und freuen uns schon heute Rust bei Ihnen in einem Projekt unter Beweis zu stellen. Stay tuned!
Weiterführende Links und Ressourcen
https://github.com/rust-unofficial/awesome-rust