Original-URL des Artikels: https://www.golem.de/news/rust-ist-die-neue-programmiersprache-besser-1606-121227.html    Veröffentlicht: 14.06.2016 12:02    Kurz-URL: https://glm.io/121227

Rust

Ist die neue Programmiersprache besser?

Rust soll C und C++ Konkurrenz machen. Wir haben uns angesehen, ob die neue Programmiersprache, mit der Mozillas neue Browser-Engine Servo geschrieben wird, wirklich das Potenzial dafür hat.

Seit Jahrzehnten sind C und C++ die verbreitetsten Programmiersprachen für die Entwicklung von Betriebssystemen und leistungskritischen Anwendungen. Diese Marktposition will nun Rust streitig machen, das die Altlasten der großen Sprachen überwinden soll, ohne ihre Stärken zu verlieren. Einen Ausblick auf die Möglichkeiten von Rust liefert Mozilla mit der neuen Browser-Engine Servo. Erste Benchmarks der Engine lassen auf ein großes Potenzial schließen.

Rust ist als Hobbyprojekt des Mozilla-Angestellten Gradon Hoare entstanden. Im Jahr 2009 begann Mozilla, das Projekt aktiv zu fördern. Die erste stabile Version der Programmiersprache und des dazugehörigen Compilers rustc erschien im Mai 2015. Dieser Compiler setzt auf LLVM auf und unterstützt bereits mehrere CPU-Architekturen. Obwohl Rust von Mozilla finanziell unterstützt wird, kommt der Hauptteil der Beiträge immer noch von der Community.

Zwischen den Welten

Rust versucht, sich am Markt der Programmiersprachen zwischen Low-Level-Sprachen wie C/C++ und hoch abstrahierten Sprachen wie C# und Java zu positionieren. Die Sprache wird wie C/C++ direkt zu Maschinencode kompiliert und ermöglicht damit einen direkten Zugriff auf die Hardware. Gleichzeitig wird ebenso wie in Java und C# darauf geachtet, dass Speicherzugriffe nicht zu Sicherheitslücken führen können.

Etwa zur gleichen Zeit, als die Entwicklung von Rust an Fahrt aufnahm, begann Google, die Programmiersprache Go zu entwickeln. Obwohl beide Sprachen eine bessere Alternative zu C/C++ sein wollen, sind sie untereinander keine unmittelbaren Konkurrenten, da Go sich viel stärker auf Parallelität und eine hohe Abstraktion konzentriert, während sich Rust eher als Programmiersprache auf Systemebene positioniert.

Hallo Rust

Vor dem obligatorischen Hello-World-Programm muss zuerst einmal der dafür nötige Compiler besorgt werden. Dieser kann hier für Windows, Linux und Mac OS X heruntergeladen werden, außerdem ist er bei manchen Linux-Distributionen bereits enthalten. Da sich die Sprache immer noch stark entwickelt, sollte gelegentlich überprüft werden, ob die verwendete Version noch aktuell ist.

Außerdem bieten die Rust-Entwickler eine Online-Entwicklungsumgebung an, mit der die Beispiele aus diesem Artikel ausprobiert werden können. Im nächsten Schritt legen wir eine neue Datei "hello_world.rs" in einem Texteditor an und schreiben Folgendes hinein:

fn main() { println!("Hello World!"); }

Das Programm kann nun durch den Aufruf von "rustc hello_world.rs" kompiliert werden und gibt beim Aufruf über die Konsole die gewünschte Nachricht aus. Wer sich tiefergehend mit Rust beschäftigen möchte, findet hier die vollständige Dokumentation der Programmiersprache.

Syntax und Programmierstil

Da sich Programmierer meistens an die Syntax und Semantik ihrer Lieblingssprache gewöhnen, hier ein paar Worte aus der Sicht eines C/C++-Programmierers. Auf den ersten Blick ähnelt die Syntax im Hello-World-Programm stark der Programmiersprache C. Wird allerdings ein größeres Projekt betrachtet, sind fast keine Ähnlichkeiten zu C mehr zu erkennen, sondern mehr zu Python und C++.

Eine Funktion, die vor allem Benutzern von Skriptsprachen bekannt vorkommen wird, ist die Unterstützung von Tupeln direkt in der Programmiersprache. Dies ermöglicht es, aus einer Funktion heraus mehrere Werte zurückzugeben.

Ebenfalls ähnlich wie bei vielen Skriptsprachen verhält sich der Operator "let", der Variablen deklariert und initialisiert. Er kann im Gegensatz zu C/C++ links und rechts des "="-Operators mehrere Elemente verarbeiten und benutzt Pattern Matching, um die jeweiligen Elemente zuzuweisen.

Anders als bei vielen verbreiteten Programmiersprachen müssen in Rust Variablen mit dem Schlüsselwort "mut" explizit als veränderbar markiert werden, während ansonsten meist Konstanten explizit als solche definiert werden müssen. Wird eine Variable ohne "mut" initialisiert und darauf geschrieben, lässt sich das Programm nicht kompilieren.

// Funktion, die ein Tupel zurückgibt fn get_tuple() -> (i8,i8) { let a = 4; let b = 2; // a und b zu Tupel zusammenfassen (a,b) } fn main() { // pattern matching, um das Tupel wieder zu trennen let (x1, x2) = get_tuple(); println!("x1: {}", x1); println!("x2: {}", x2); // Werte sind automatisch Konstanten let ro = 3; // Variable als veränderbar deklarieren let mut rw = 2; rw = 10; // zulässig // ro = 7; unzulässig, }

Sichere und effiziente Speicherverwaltung

Bei Rust muss sich der Programmierer wenig mit der Speicherverwaltung auseinandersetzen, alle primitiven Datentypen werden am Stack abgelegt, die restlichen auf dem Heap. Allerdings steht, um Daten explizit am Heap abzulegen, der Zeigertyp "Box" zur Verfügung.

Darüber hinaus ist Rust mit dem Ziel erstellt worden, eine sichere Sprache für parallel ablaufende Programme zu sein und dabei dennoch die Geschwindigkeit von C/C++ zu erreichen. Dazu werden viele unsichere Speicherzugriffe bereits zur Kompilierzeit erkannt, was der Sprache eine aufwendige Laufzeitumgebung erspart und sie so effizienter macht als andere High-Level-Programmiersprachen wie C# und Java. Rust verhindert außerdem sogenannte Data Races. Diese treten auf, wenn mehrere Threads dieselben Datenbereiche bearbeiten oder mehrere Zeiger auf einem Speicherbereich existieren, was zu unerwünschtem Verhalten des Programms führen kann.

Rust setzt für die sicheren Speicherzugriffe auf ein System von Zeigerbesitzrechten (Pointer Ownership) und Zeigerlebenszeiten (Pointer Lifetime). Durch Pointer Ownership wird garantiert, dass immer nur eine Referenz auf ein Objekt existiert. Durch Pointer Lifetime wird sichergestellt, dass, sobald die Referenz auf ein Objekt den Arbeitsbereich verlässt, dessen Speicher freigegeben wird.

Außerdem kann eine Funktion einen Zeiger ausborgen (Borrowing), dabei erhält die Funktion vorübergehend eine Referenz auf die Daten, die nach dem Verlassen der Funktion dann nicht freigegeben wird und somit weiter verwendet werden darf. Rust kann mit diesem System allerdings nur eine bestimmte Klasse von Fehlern in der Speicherverwaltung verhindern.

Dazu zählen Nullzeigerdereferenzierung, Zugriff auf bereits freigegebenen Speicher (Use after free), Lesen von nicht initialisiertem Speicher (liefert undefinierte Daten) sowie mehrfaches Freigegeben von Speicher (double free). Explizit nicht zu dieser Gruppe gehören Integer Overflows und das Lesen zum Beispiel über eine Array-Grenze hinaus (Out of Bounds Access). Letzteres ist im Gegensatz zu C/C++ aber definiertes Verhalten und führt zu einem "panic", einem sicheren Beenden des Programms.

Hier ein kurzes Beispiel zu Zeigern in Rust:

Im obigen Beispiel sind Pointer Ownership, Lifetime und Borrowing praktisch dargestellt. Zuerst wird ein Vektor auf dem Heap abgelegt und eine Referenz darauf in "v1" gespeichert, "v1" "gehört" nun der Funktion "main()". Der Funktion "borrow_var()" wird nun mit dem "&"-Operator eine Referenz geborgt, sie wird in der Funktion verwendet und kann anschließend wieder in "main()" benutzt werden.

Im zweiten Schritt wird nun "v1" ohne den "&"-Operator übergeben, dies bedeutet, "v1" gehört nun "move_var()". Endet der Arbeitsbereich der Funktion, endet auch die Lebenszeit von "v1" und der Speicher wird freigegeben. Rust verhindert auch, dass "v1" innerhalb von "main()" weiterverwendet wird. Da der Speicher ja bereits freigegeben wurde, führt ein Zugriff darauf zu einem Fehler beim Kompilieren.

In C wäre es ohne Fehler beim Kompilieren möglich, den Speicher einer Variablen freizugeben und anschließend darauf zuzugreifen. Prompt hätte man einen Absturz des Programms oder eine schwere Sicherheitslücke, da das Betriebssystem den Speicher vielleicht schon anderweitig verwendet hat.

Such dir deine Garantien aus!

In dem Beispiel wurden nur einfache Referenzen benutzt, die vom Rust-Compiler statisch geprüft werden können. In der Standardbibliothek finden sich allerdings auch viele Implementierungen intelligenter Referenzen, die jeweils bestimmte Eigenschaften garantieren und andere undefiniert lassen, um schneller zu sein.

So zum Beispiel "Rc", der einem Zeiger mit Referenzzählung entspricht. Dieser garantiert zur Laufzeit, dass der Speicher erst dann freigegeben wird, wenn die letzte Referenz darauf freigegeben wurde, aber nicht, dass keine zyklischen Referenzen entstehen, was zu Speicherlecks führt.

Dieses Verhalten funktioniert jedoch nur innerhalb eines Threads, für Anwendungen mit mehreren Threads oder andere Sonderfälle existieren wiederum verschiedene andere Referenztypen, die hier detailliert aufgelistet sind.

Dieses System ähnelt sehr stark den intelligenten Zeigern in C++, die eine feinkörnige Auswahl aus nötigen Garantien ermöglichen und somit die Ausführungsgeschwindigkeit hochhalten. Java und C# gehen hier den Weg über einen Garbage Collector, der zur Laufzeit immer wieder nicht mehr referenzierte Objekte aus dem Speicher entfernt; natürlich geht das Aufräumen dann auch auf Kosten der Verarbeitungsgeschwindigkeit.

Die dunkle Seite von Rust

Zusätzlich zum eigentlichen Rust, "Safe Rust", existiert auch noch eine "Unsafe Rust"-Umgebung, eingeleitet durch das Schlüsselwort "unsafe". In dieser Umgebung sind einige Regeln aufgehoben und es können potenziell unsichere Speicherzugriffe durchgeführt werden.

Die wichtigste Möglichkeit, die sich dadurch ergibt, ist, andere unsichere Funktionen aufzurufen. Funktionen aus C-Bibliotheken sind in Rust per Definition immer "unsafe", weil deren Speicherverwaltung nicht vom Rust-Compiler kontrolliert werden kann. Außerdem ist es nur in der Unsafe-Umgebung möglich, Raw-Pointer, also echte Zeiger wie in C, zu dereferenzieren. Erst diese Fähigkeit ermöglicht es in Rust, etwa Gerätetreiber und Kernel zu schreiben, da von der Hardware oft Raw-Pointer als Zeiger auf Daten geliefert werden. Auch hier besteht wieder das Risiko von Zugriffen auf undefinierte Speicherbereiche etwa durch fehlerhafte oder falsch konfigurierte Hardware und Programmierfehler.

Für die meisten Anwendungen sollten allerdings die Möglichkeiten von Rust ohne Risiken von "unsafe" ausreichen. Für alle anderen Fälle gilt es, die Dokumentation für unsafe sehr genau zu studieren. Ebenfalls lesenswert ist das Rustonomicon, das sich hauptsächlich mit den fortgeschrittenen Möglichkeiten von Rust beschäftigt.

Dinge, die das Programmieren einfacher machen

Rust bringt mit "rustdoc" eine praktische Möglichkeit, den geschriebenen Code direkt zur Dokumentation zu verwenden. Dieses Feature wird allerdings von anderen Programmiersprachen wie Java und C# ebenfalls geboten.

Ein weiteres wichtiges Kriterium, um eine Programmiersprache in einem Projekt einzusetzen, ist die Verfügbarkeit von Bibliotheken. In diesem Punkt muss Rust derzeit noch passen. Sie bietet zwar eine umfangreiche Standardbibliothek und kann auf C-Bibliotheken zurückgreifen, richtige Rust-Bibliotheken mit ihren Vorteilen findet man derzeit aber noch selten. Abhilfe schaffen in diesem Fall Sprachanbindungen wie etwa für Python, womit sich Funktionen aus anderen Sprachen verwenden lassen.

Da Rust als Programmiersprache auf Systemebene entwickelt wurde, ist die Sprache sehr auf Portabilität ausgerichtet. Beispielsweise gibt es Anstrengungen, um Rust auf dem AVR-Mikrocontroller des Arduino lauffähig zu machen. Dabei kann dann allerdings nicht auf die Rust-Standardbibliothek zurückgegriffen werden, da ein Großteil dieser ein Betriebssystem voraussetzt.

Rust ist definitiv eine Alternative zu C/C++. Es wurde von Anfang an mit dem Ziel entwickelt, die typischen Fehler in der Speicherverwaltung von C/C++ zu vermeiden und ist dabei ebenfalls sehr portabel. Wie bei jeder jungen Programmiersprache muss sich jedoch erst ein größeres Ökosystem aus Bibliotheken und Tools aufbauen, damit die Sprache langfristig bestehen kann. Die bisherige Entwicklung hin zu mehr Fehlervermeidung schon in der Programmiersprache kann allerdings nur befürwortet werden und hat durch Rust definitiv einen Entwicklungsschub bekommen.  (crr)


Verwandte Artikel:
Programmiersprache: Rust-Community will Sprache konsolidieren   
(24.04.2019, https://glm.io/140854 )
Progammiersprache: Rust 1.33 ermöglicht Anpinnen von Speicherbereichen   
(01.03.2019, https://glm.io/139725 )
Programmiersprache: Rust läuft auf 14 Debian-Architekturen   
(05.11.2018, https://glm.io/137512 )
Programmiersprache: Rust 1.30 überarbeitet Modulsystem und erweitert Makros   
(30.10.2018, https://glm.io/137391 )
Programmiersprache: Rust 1.26 verbessert Traits, Match und Main   
(11.05.2018, https://glm.io/134344 )

© 1997–2020 Golem.de, https://www.golem.de/