Die Programmiersprache Rust - Ein Überblick Michael Andonie 29. April 2015 Inhaltsverzeichnis 1 Abstract 2 2 Einleitende Gedanken 3 3 Grundidee, Verwendungsbereich und Paradigmen der Sprache 3 4 Entstehung und Entwicklung der Sprache 4 5 Aufbau und Verwendung der Sprache 5.1 SDK und logistische Aspekte . . . . . . . . 5.2 Syntax und Vergleich mit C . . . . . . . . . 5.2.1 Hello World . . . . . . . . . . . . . . 5.2.2 Fakultätsfunktion und Kontrollfluss 5.2.3 Structs . . . . . . . . . . . . . . . . . . . . . 5 5 5 5 6 8 6 Ausgewählte Konzepte der Sprache 6.1 Nicht verwendete Variablen und Funktionen . . . . . . . . . . . . 6.2 Ownership und Borrowing . . . . . . . . . . . . . . . . . . . . . . 6.3 Nebenläufigkeit in Rust . . . . . . . . . . . . . . . . . . . . . . . 9 9 9 11 7 Abschließende Gedanken 14 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 Abstract Die Programmiersprache Rust ist eine innovative Paradigmenkombination funktionaler und imperativer Aspekte, die sowohl systemnahe Programmierung als auch hohe Ausfallsicherheit ermöglicht. Diese Arbeit gibt einen Überblick über die Sprache mit dem Ziel, einen praktikablen Einstieg in die Sprache zu ermöglichen. Hierzu werden vornehmlich die technischen und konzeptuellen Grundlagen der Sprache sowie anschließend ausgewählte und bemerkenswerte Konzepte von Rust behandelt. 2 2 Einleitende Gedanken In der Geschichte der Programmiersprachen finden sich immer wieder Initiativen, Problematiken bestehender Sprachen zu bekämpfen und die Vorteile verschiedener Programmiersprachen zu kombinieren. Im Zuge dieser Bestrebungen entstanden Sprachen wie D, Avail, Ceylon oder Dart. Die Programmiersprache Rust nimmt hierbei ebenfalls eine Rolle ein. Sie ist eine Bestrebung, systemnahe Programmierung mit funktionalen und imperativen Paradigmen zu verknüpfen. Dieses Zusammenspiel klingt zunächst verlockend: Die Zuverlässigkeit, Lesbarkeit und Wartbarkeit funktionaler Codes verknüpft mit der Vielseitigkeit und Anpassbarkeit von imperativen auf systemnaher Ebene öffnet Möglichkeiten, die Sprachen wie Haskell bisher vorenthalten sind. Diese Arbeit gibt einen Überblick über die Programmiersprache Rust, ohne ein wertendes Urteil hierüber zu formulieren. Zum einen sprengt eine fundierte, kritische Analyse den Rahmen dieser Bachelor-Seminararbeit; zum anderen ist eine Bewertung bei dieser noch sehr jungen Sprache zu früh angesetzt. 3 Grundidee, Verwendungsbereich und Paradigmen der Sprache Die Sprache Rust ermöglicht die Entwicklung hochperformanter Anwendungen in einer modernen und systemnahen Sprache mit Stabilitätsaspekten, die über die von z.B. C weit hinaus gehen. Beim Design der Sprache stehen drei wesentliche Aspekte im Vordergrund: [4] [1] • Performanz in der Ausführung • Ausfallsicherheit • Elemination von Problemsituationen durch Nebenläufigkeit (z.B. Race Conditions) Rust zielt darauf ab, sichere und performante Anwendungen für das Internet zu ermöglichen. Der Initiator von Rust, Graydon Hoare, stellte in einem Interview fest, dass mit der Entwicklung des Internets sich die Anforderungen erhöht haben an nebenläufigen und sicheren Programmen (im Sinne von Ausfallsicherheit wie Sicherheit vor Eingriffen Dritter). Weiterhin erklärt er, dass sich mit dieser Änderung die Vorteile von C und C++ gegenüber alternativen Sprachen verändern. Er stellt somit in Frage, ob die Verwendung dieser Sprachen in ihrer heutigen Form noch zeitgemäß ist. [2] Rust ist genau für die Domäne Internet gestaltet. Die Sprache soll ein hohes Maß an Sicherheit, Nebenläufigkeit und Performanz für die komplexen Anwendungen des modernen Internets ermöglichen. Im Sinne dieser drei Hauptziele lassen sich folgende Kernaspekte der Sprache feststellen: • Multi-Paradigmen Die erste und wichtigste Feststellung beim Betrachten der verschiedenen Aspekte von Rust ist, dass diese Sprache Anleihen von mehreren (klassischerweise gegensätzlichen) Paradigmen macht. So entsteht eine Kombination von Aspekten, die (nur) gemeinsam sinnvoll auf die drei Hauptziele der Sprache hinarbeiten können. 3 • Nebenläufig Mit der steigenden Anzahl an Prozessorkernen pro Maschine und der Parallelisierung von Operationen über mehrere Rechner ist Nebenläufigkeit ein Thema, dessen Relevanz in den kommenden Jahren weiter steigen wird. Probleme bei der Parallelisierung von Operationen (wie z.B. Race Conditions oder Deadlocks) sind Programmierern geläufig und bei der Softwareentwicklung wird adäquate Zeit in die Verhinderung solcher Zustände investiert. Rust beugt diesen Problemen, die durch Nebenläufigkeit und fehlender Threadsicherheit entstehen können, bereits zur Kompilierzeit vor. [4] Dies wird an späterer Stelle weiter ausgeführt. • Funktional Funktionale Programmiersprachen bieten enorme Vorteile: Codes sind (relativ) schlank, übersichtlich und präzise. Besonderheiten wie die Lazy Evaluation ermöglichen das Verfassen von performanten Logiken, deren direktes Analogon in imperativen Programmiersprachen höchst ineffizient wäre. Um solche Vorteile für systemnahe Entwickler nutzbar zu machen, implementiert die Programmiersprache Rust funktionale Aspekte, wie Typinferrenz, Typklassen, Pattern Matching, High Order Functions und einen starken Fokus auf unveränderbare Variable Bindings. [1] • Imperativ Imperative Sprachen sind eine direkte Übersetzung prozeduraler Denkweise. Die meisten Vorgänge der Client-Server-Kommunikation lassen sich hervorragend in prozeduralen Diagrammen beschreiben und somit (relativ) unkompliziert in eine imperative Sprache übertragen. Damit ist eine imperative Sprache nahezu eine Notwendigkeit für die Implementierung von streng definierten Client-Server-Anwendungen. Rust implementiert als Programmiersprache wesentliche imperative Aspekte, wie die schrittweise Abarbeitung von Ausdrücken und Kontrollstrukturen. [4] 4 Entstehung und Entwicklung der Sprache Nachdem der grundlegende Charakter der Programmiersprache Rust ausreichend erörtert ist, sei im Folgenden eine kurze Zusammenfassung des Fortschritts angeführt, den Rust bisher durchlebt hat. Die Entwicklung von Rust begann bereits 2006 als privates Projekt des Mozilla-Entwicklers Graydon Hoare. Der Language Engineer stellte Rust 2009 prototypisch seinem derzeitigen Manager vor. Mozilla war interessiert und finanzierte die weitere Entwicklung und stellte ein Entwicklerteam. Rust wurde Teil des Langzeitprojekts Servo, das die umfassende Wartung der Mozilla Browser zum Ziel hat. Seit 2010 existiert ein Compiler, seit 2011 kann sich Rust selbst kompilieren. [2] [5] Der erste offizielle Release fand Anfang 2012 mit Version 0.1 statt. Seitdem hat die Sprache fundamentale semantische und syntaktische Änderungen 4 durchlaufen und hat nun, im Mai 2015, mit dem Release der Version 1.0 einen wesentlichen Meilenstein abgeschlossen. [5] 5 Aufbau und Verwendung der Sprache Im Folgenden wird die Sprache an sich und deren Verwendung erläutert, mit dem Ziel, einem Programmierer eine effiziente Einführung in Rust zu geben. Hierbei ist unbedingt zu beachten, dass gerade erst der 1.0-Release der Sprache veröffentlicht wurde. Die folgenden Aspekte sind hiermit vollständig kompatibel. [1] [5] 5.1 SDK und logistische Aspekte Bevor eigentlicher Code betrachtet wird, wird an dieser Stelle ein Blick über die allgemeine Verwendungsweise der Sprache gegeben. Die (Open-Source-orientierte) Entwicklung der Sprache ist über GithHub organisiert. In einer öffentlichen Organisation sind dort alle Module bzw. Projekte der Sprache erreichbar. Hierzu gehören unter Anderem das SDK der Sprache, ein Paketmanager, zusätzliche Libraries und Beispielcodes. [6] Die Einrichtung für Rust-Entwickler funktioniert unkompliziert über ausführbare Setups (für Linux/Mac und Windows). Hiernach sind alle nötigen Programme (unter Anderem Compiler und Paketmanager) installiert und einsatzbereit. Nach erfolgreichem Setup können einzelne Rust-Files direkt über den Compiler (rustc-Kommando) und Programme aus mehreren Rust-Files über den Paketmanager (cargo-Kommando) in ausführbaren Maschinencode übersetzt werden. [4, 2. Getting Started] 5.2 Syntax und Vergleich mit C Nachdem nun sowohl die Grundlagen wie auch der allgemeine Workflow beim Entwickeln in Rust geklärt ist, kann man mit ausreichendem Hintergrundwissen die eigentliche Sprache in ihrer Syntax und Verwendungsweise betrachten. Hierzu seien im Folgenden kommentierte Code-Snippets aufgeführt. [4] 5.2.1 Hello World In der Lerntradition aller Programmiersprachen ist das erste Beispiel natürlich ein Hello World. Als solches gibt es Hello World! auf der Konsole aus. Rust C f n main ( ) { // D e f i n e ( n a t u r a l l y immutable ) v a r i a b l e s . // Comparable t o f u n c t i o n a l l a n g u a g e s l e t g r e e t i n g = ” H e l l o ” ; // g r e e t i n g : &s t r l e t r e c e p t o r = ”World” ; #i n c l u d e <s t d i o . h> // P r i n t t e x t t o t h e c o n s o l e p r i n t l n ! ( ” {} { } ! ” , g r e e t i n g , r e c e p t o r ) ; } i n t main ( ) { char ∗ g r e e t i n g = ” Hello ” ; c h a r ∗ r e c e p t o r = ”World” ; p r i n t f ( ”%s %s ! \ n” , greeting , receptor ) ; return 0; } Trotz kleinerer Unterschiede ist der Aufbau des Hello World Programm mit CGrundwissen sehr leicht nachvollziehbar. 5 • Das fn Schlüsselwort fällt sofort auf. Es geht allen Funktionsdefinitionen in Rust voraus. • Variablen (im Sinne klassischer imperativer Sprachen wie C) können durch das let Schlüsselwort definiert werden. Hierdurch wird ein Wert an die Variable gebunden. Der Wert so definierter Variablen lässt sich zunächst nicht ändern; bei imperativer Betrachtungsweise kann man sie als final bezeichnen. Diese strikte Herangehensweise zieht direkte Parallelen zu funktionalen Sprachen wie Haskell, in denen die selben Einschränkungen bei durch let gebundenen Werten gelten. Veränderbare Variablen lassen sich durch das folgende Beispiele) definieren. mut-Schlüsselwort (vgl. Durch die hier definierten Variablen wird auch die Typinferrenz von Rust deutlich. Der (streng definierte) Typ der Variable greeting in Rust lässt sich (in geläufiger Pointer-Semantik) darstellen als &str, jedoch wird der Typ nicht explizit genannt, da er implizit erkennbar ist. Der Typ kann explizit genannt werden: let greeting : &ptr = ”Hello”;[4, 5.19. Strings] • println!(...) ist ein Sonderfall. Das Ausrufezeichen (”!”) am Ende dieses (vermeintlichen) Methodenaufrufs signalisiert, dass hierbei nicht eine Funktion println aufgerufen wird, sondern dass ein entsprechendes Macro ausgeführt wird. Ansonsten ist die Funktionsweise direkt vergleichbar mit C und nachvollziehbar. Die öffnende und schließende geschwungene Klammer sind Platzhalter für eine String-Repräsentation des entsprechenden Wertes, der in den folgenden Parametern geliefert wird. 5.2.2 Fakultätsfunktion und Kontrollfluss Als weiteres Beispiel für die grundlegende Verwendungsweise möchte ich die Fakultätsfunktion (als geläufiges Beispiel) anführen. Hierbei werden veränderbare Variablen und Kontrollfluss verwendet. 6 Rust C // I t e r a t i v e f n f a c t o r i a l o n e ( n : u32 ) −> u32 { l e t mut i = 1 u32 ; l e t mut r e s u l t = 1 u32 ; w h i l e i <= n { r e s u l t ∗= i ; i += 1 ; } return r e s u l t ; } #i n c l u d e <s t d i o . h> // I t e r a t i v e int f a c t o r i a l o n e ( int n) { int i = 1; int result = 1; w h i l e ( i <= n ) { r e s u l t ∗= i ; i ++; } return r e s u l t ; } // R e c u r s i v e f n f a c t o r i a l t w o ( n : u32 ) −> u32 { i f n <= 1 { 1 } else { n ∗ f a c t o r i a l t w o (n − 1) } } // R e c u r s i v e int factorial two ( int n) { i f ( n <= 1 ) { return 1; } else { r e t u r n n ∗ f a c t o r i a l t w o ( n−1) ; } } f n main ( ) { let n = 8; p r i n t l n ! ( ” Result of {}! ( i t ) i s {}. ” , n , f a c t o r i a l o n e (n) ) ; p r i n t l n ! ( ” Result of {}! ( rec ) i s {}. ” , n , factorial two (n) ) ; } i n t main ( ) { int n = 8; p r i n t f ( ” R e s u l t o f %i ! ( i t ) i s %i . \ n” , n , f a c t o r i a l o n e (n) ) ; p r i n t f ( ” R e s u l t o f %i ! ( r e c ) i s %i . \ n” , n , factorial two (n) ) ; return 0; } Weitere Grundaspekte der Grammatik und Semantik von Rust werden hier deutlich. • Der Rückgabetyp einer Funktion wird (vergleichbar mit Haskell) nach der geschlossenen Klammer im Kopf und vorangehendem −> angegeben. Wird dieser Pfeil weggelassen (z.B. in der Main Funktion), so wird implizit der leere Typ mit Element () als Datentyp angenommen. Parameter von Funktionen werden im Format ”name : typ”(und von Klammern getrennt) formuliert. • Mit ”mut”(mutable) gekennzeichnete Variablen lassen sich im Wert verändern. Sie funktionieren (sinngemäß) wie Variablen in C und lassen sich genauso verwenden. Es sei angemerkt, dass der ”++Operator in Rust nicht wie in C exisitert. • u32 steht für ”unsigned 32-bit integer”. Es gibt Integer-Bitgrößen von 8 bis 64. i16 ist ein ”signed 16-bit integer”. Entsprechend werden Werte in angegeben, indem nach der numerischen Repräsentation direkt der Bezeichner des Datentyps folgt. 42i64 bezeichnet also die Zahl 42 als Ganzzahl mit Vorzeichen. • Die if-Kontrollstruktur hat eine implizite Rückgabe; sie ist selbst ein Ausdruck. Daher ist kein explizites return notwendig. Damit müssen auch alle Zweige der Kontrollstruktur eine Rückgabe des selben Typs aufweisen. 7 5.2.3 Structs In Rust gibt es mehrere Arten, Datenstrukturen zu definieren. Im Folgenden wird die Allgemeinste betrachtet. s t r u c t Point { x : i32 , y : i32 } s t r u c t CustomPoint { l o c a t i o n : Point , value : f32 } impl P o i n t { // This p e r f o r m s an a r b i t r a r y a l t e r a t i o n f n move fun(&mut s e l f ) { s e l f . x += 5 ; } } f n main ( ) { // c r e a t i n g an i n s t a n c e l e t custom = CustomPoint { v a l u e : 10 f 3 2 , l o c a t i o n : P o i n t {x : 2 0 , y : 1 0 } } ; // R e t r i e v i n g v a l u e s f o r l o c a t i o n & v a l u e // v i a d e s t r u c t u r i n g l e t CustomPoint { l o c a t i o n : mut p o i n t , v a l u e : v a l } = custom ; p o i n t . move fun ( ) ; p r i n t l n ! ( ” L o c a t i o n a t ( { } , { } ) , Value={}” , point . x , point . y , val ) ; // Output w i l l be : ” L o c a t i o n a t ( 2 0 , 1 0 ) , Value =10” } In diesem Beispiel verzichte ich bewusst auf das Analogon in C. Es gibt keine saubere Entsprechung zu Funktionen, die in Strukturen definiert sind. Eine mögliche Umsetzung wäre es, die Funktion außerhalb zu definieren und über einen Funktionspointer innerhalb der Struktur zu referenzieren. Beim Betrachten von Structs in Rust kann man folgende Beobachtungen machen: • Werte innerhalb eines Struct werden durch Kommata getrennt und in der von Rust gewohnten Syntax definiert. • Mit dem impl-Schlüsselwort lassen sich Methodensets zu Structs definieren. Dies ist so nicht möglich in C und ist einer der wesentlichen Stützpfeiler für Rusts objektorientierte Aspekte. Innerhalb von so definierten Funktionen ist die Referenz auf das ausführende Objekt nicht selbstverständlich. Ein &self in den Paramatern stellt sicher, dass die ausführende Instanz auf sich selbst über das self Keyword zugreifen kann. Nachdem in der Methode move fun der Wert des ausführenden Objekts geändert wird, muss der Parameter auch das Schlüsselwort mut enthalten. • Die Initialisierung neuer Instanzen orientiert sich syntaktisch stark an 8 der Strukturdefinition. Werte können in beliebiger Reihenfolge über deren Identifier definiert werden. • Durch Destrukturierung können die Werte eines Structs neu referenziert werden. Das Vorgehen im Beispiel macht hierbei die Parallelen zu funktionalen Sprachen (durch Pattern Matching) deutlich: Die Werte der bekannten Struktur (value, location) werden hier in neuen Variablen (point, val) gespeichert. Hierbei muss die Referenz auf das Punktobjekt als mut gekennzeichnet werden, da anschließend die Funktion move fun aufgerufen wird und diese verlangt, dass die ausführende Struktur veränderbar ist. 6 Ausgewählte Konzepte der Sprache Nachdem nun ein ausreichendes Verständnis über die Grundlagen der Sprache besteht, können einige bemerkenswerte Konzepte der Sprache Rust betrachtet werden. Die hier vorgestellten Konzepte heben Rust - vor allem hinsichtlich der drei zentralen Designziele - im Vergleich zu anderen Programmiersprachen hervor. 6.1 Nicht verwendete Variablen und Funktionen Gerade im professionellen Kontext hochkomplexer Server-Client-Anwendungen ist Codequalität ein stetig präsentes Thema. Wesentliche Entscheidungen beim Design von Rust spielen derartigen Paradigmen sauberen Quellcodes zu. Zum Beispiel warnt der Rust-Compiler (standardmäßig) bei Existenz nicht verwendeter Funktionen / gebundener Variablen: [7, 4. Variable Bindings] f n main ( ) { l e t u s e d v a r i a b l e = ”H” ; l e t u n u s e d v a r i a b l e = ”W” ; //<− c o m p i l e r warning let // u n u s e d b u t m a r k e d v a r i a b l e = ” I ’m u s e l e s s . ” ; /\ NO c o m p i l e r warning p r i n t l n ! ( ” {} e l l o o r l d ! ” , u s e d v a r i a b l e ) ; } f n u n u s e d f u n c t i o n ( ) { // <− c o m p i l e r warning // Nothing i n t e r e s t i n g happens h e r e . } Dieser Code wird bei Standardeinstellungen nicht kompiliert. Nicht verwendete Variablen und Funktionen müssen mit vorangehendem Unterstrich markiert werden. Somit signalisiert der Rust-Entwickler sowohl dem Compiler als auch anderen Entwicklern (die möglicherweise mit Code-Wartung beauftragt sind), dass die entsprechenden Elemente des Codes bewusst nicht verwendet werden. 6.2 Ownership und Borrowing Die Pointer-Arithmetik bringt enorme Flexibilität in die systemnahe Entwicklung. Dies hat jedoch auch eine Schattenseite: Schnell passiert es, dass bei 9 dynamischen Speicherveränderungen Pointer bestehen bleiben, bei denen die Referenz-Ressource nicht mehr gültig ist; ein Zugriff unter diesen Umständen kann zu erheblichen (und bemerkenswert verwirrenden) Programmfehlern führen. Rust bietet ein Konzept, dass diese Dangling Pointers verhindert: Ownership. [4, 5.8. Ownership] Hierzu sei zunächst die Allokation von Speicherplatz auf dem Heap in Rust erläutert. Hierzu gibt es die Struktur Box. Sie ist semantisch Äquivalent zu einem Malloc-Aufruf. Rust C f n main ( ) { l e t i = Box : : new ( 1 0 ) ; println !( ” I have a {} on t h e Heap . ” , i ) ; } i n t main ( ) { i n t ∗ i = malloc ( s i z e o f ( i n t ) ) ; ∗ i = 10; printf ( ” I have a %i on t h e Heap . ” , ∗ i ) ; return 0; } In Rust hat jedes Objekt auf dem Heap, und damit jede Box, genau einen Besitzer. Wird einer neuen Variable ein Wert übergeben, so wird damit auch das Eigentum an diesem Wert übertragen; die neue Variable dessen Besitzer. Nur der aktuelle Besitzer eines Werts kann auf sie zugreifen. Folgendes Beispiel illustriert dieses Konzept: f n main ( ) { // A l l o c a t e s p a c e f o r 1 i n t on t h e heap // Value w i l l be 5 f o r now . l e t mut a = Box : : new ( 5 ) ; p r i n t l n ! ( ” I n my Box A I have a { } . ” , a ) ; ∗ a = 3 ; // A r b i t r a r y v a l u e change p r i n t l n ! ( ” I n my Box A I have a { } . ” , a ) ; // A s s i g n a t o b . // This moves o w n e r s h i p o f our Box from a t o b let b = a; p r i n t l n ! ( ” I n my Box B I have a { } . ” , b ) ; // This w i l l p r o v o k e a c o m p i l e r e r r o r a s a no // l o n g e r owns t h e i n s t a n c e i t ’ s p o i n t i n g t o . p r i n t l n ! ( ” I n my Box A I have a { } . ” , a ) ; } Dies stellt sicher, dass (möglicherweise) widersprüchliche Zugriffe aus Versehen durch verschiedene Variablen auf die selbe Ressourcen kaum noch möglich sind. Jedoch ist nicht immer ein so strikter Umgang mit den Werten erwünscht. Daher gibt es eine Möglichkeit, das Eigentum an einem Wert nur kurzzeitig zu verleihen. Dies bezeichnet Rust als Borrowing. Während beim Übertrag von Werten das Besitzverhältnis hieran dauerhaft geändert wird, findet beim Übertrag von Pointern nur eine Leihe statt. Solange ein Pointer verliehen ist, kann das Eigentum hieran nicht verschoben werden. Das vorige Beispiel lässt sich mit wenigen Griffen also so erweitern, dass der Code ausführbar wird: 10 f n main ( ) { l e t mut a = Box : : new ( 5 ) ; ∗a = 3 ; //Make b a P o i n t e r i n s t e a d o f t h e a c t u a l v a l u e . // Now we can borrow . l e t b = &a ; p r i n t l n ! ( ” I n my Box B I have a { } . ” , ∗b ) ; // This w i l l no l o n g e r p r o v o k e an e r r o r , a s o w n e r s h i p s t i l l // l i e s w i t h i n a . p r i n t l n ! ( ” I n my Box A I have a { } . ” , a ) ; //Now THIS l i n e w i l l p r o v o k e a c o m p i l e −time e r r o r . // Ownership can ’ t be moved a s l o n g a s a i s borrowed // s o m e p l a c e let c = a; } Versucht man nun aber, einen Wert zu übertragen, während dieser noch an anderer Stelle verliehen ist, gibt es einen Compiler-Fehler. In diesem Beispiel lässt sich dies lösen, in dem die Variable b nur innerhalb eines inneren Blocks gültig wäre: f n main ( ) { l e t mut a = Box : : new ( 5 ) ; ∗a = 3 ; { l e t b = &a ; p r i n t l n ! ( ” I n my Box B I have a { } . ” , ∗b ) ; } p r i n t l n ! ( ” I n my Box A I have a { } . ” , a ) ; l e t c = a ; //Now t h i s l i n e works , t o o . } Bevor der Variable c ein Wert zugewiesen wird, ist die Gültigkeit aller Leihgaben von a abgelaufen. Daher kann nun das Eigentum bedenkenlos übertragen werden. Der Compiler akzeptiert diesen Code. Zusammenfassend kann also in Rust bereits zur Compile-Zeit garantiert werden, dass alle Referenzen stets auf gültige Objekte zeigen. Dies passiert ohne intensive Garbage Collection oder den Einschnitt der Pointer-Arithmetik und stellt somit einen enormen Vorteil in den Bereichen Ausfallsicherheit und Sicherheit vor Exploits dar. Die starken Annahmen, die über die Verwendung der Pointer zur Compile-Zeit getroffen werden können, machen dies möglich. 6.3 Nebenläufigkeit in Rust Nebenläufigkeit wurde als kritisches Thema für Software-Entwickler bereits angesprochen. Rust ermöglicht, wie die meisten imperativen Programmiersprachen, die Implementierung nebenläufiger Vorgänge. Im Gegensatz zu den meisten Sprachen jedoch bietet Rust dank seines strengen Typsystems die (weitgehende) Sicherstellung korrekter - also Fehlerfreier - Nebenläufigkeit. [4, 4.6. Concurrency] Zunächst sei die Erstellung unterschiedlicher Threads in Rust geklärt. Dies ist die Grundlage für Nebenläufigkeit. Ein Thread lässt sich einfach isntanziieren: 11 use std : : thread ; f n main ( ) { l e t c h i l d = t h r e a d : : spawn ( | | { l e t mut c = 0 ; w h i l e c <= 100 { //Do s o m e t h i n g a r b i t r a r y c += 1 ; } ” I c h habe f e r t i g . ” }) ; p r i n t l n ! ( ” {} ” , c h i l d . j o i n ( ) . unwrap ( ) ) ; } Hierzu seien erläuternde Anmerkungen angeführt: • Durch das tiert. use Keyword wird das Thread-Modul der Sprache impor- • Die Funktion spawn erzeugt und startet einen neuen Thread. Ihr wird als Parameter die auszuführende Logik als parameterlose Funktion übergeben. • || eröffnet eine anonyme Lambda-Funktion. Zwischen den senkrechten Strichen können Parameter für die Funktion definiert werden. Ein expliziter Rückgabetyp ist nicht erforderlich, kann aber angegeben werden. Durch das Typsystem ist der Rückgabetyp (in diesem Fall &str) implizit herleitbar. • Die Funktion join hält - wie bei vergleichbaren Sprachen - den ParentThread an, bis der Child-Thread ausgeführt wurde. Die Methode unwrap gibt das Ergebnis der Ausführung des Child-Threads aus. Mit diesen Grundlagen kann nun ein Blick auf echt nebenläufige Prozesse in Rust geworfen werden. Aus platzgründen wird als einziger Aspekt Shared Memory angeführt. Mehrere Threads, die sich dieselben Ressourcen im Heap teilen, bergen altbekanntes Störungspotential. Rust-Programme bieten zwar nicht automatisch perfekte Speicher-Sicherheit, jedoch werden die typischen Problemursachen durch Rusts Ownership System erkannt und vom Compiler nicht akzeptiert. Hierzu sei folgender Code angeführt: [4, nach Beispiel in 4.6.] use std : : thread ; f n main ( ) { l e t data = v e c ! [ 2 u32 , 3 , 8 ] ; for i in 0 . . 2 { t h r e a d : : spawn ( move | | { for j in 0 . . 5 { data [ i ] += 1 0 ; } }) ; } t h r e a d : : s l e e p m s ( 1 0 0 ) ; // A r b i t r a r y Waiting Time . p r i n t l n ! ( ” L i s t : [ { } , { } , { } ] ” , data [ 0 ] , data [ 1 ] , data [ 2 ] ) ; } 12 Zu diesem Code sind zwei Erläuterungen angebracht: • Das Makro vec! erstellt einen Vektor (also eine dynamische Liste) mit den Werten [2,3,8]; • Das move-Keyword signalisiert, dass die Ownership der äußeren Variable data in die Lambda-Funktion übertragen wird. Dies ist notwendig, da diese Funktion den Gültigkeitsbereich der main-Funktion überleben könnte. Daher muss, um data hierin zu verwenden, das Eigentum an der Struktur an die Funktion übertragen werden. Dieser Code ist nicht kompilierbar. Da mehrere Threads gleichzeitig auf data zugreifen können, kann auch jeder dieser Threads respektive das Eigentum an data beanspruchen. Dies kann zu Speicherinkonsistenzen führen, wie man sie von Race Conditions her kennt. Damit niemals mehr ein Thread auf einmal Eigentum an der Struktur beansprucht, muss also sichergestellt werden, dass stets maximal ein Thread auf die Ressource zugreift. Voll implementiert sieht dies in Rust folgendermaßen aus: u s e s t d : : s y n c : : { Arc , Mutex } ; use std : : thread ; f n main ( ) { l e t a r c = Arc : : new ( Mutex : : new ( v e c ! [ 2 u32 , 3 , 8 ] ) ) ; for i in 0 . . 2 { l e t mutex = a r c . c l o n e ( ) ; t h r e a d : : spawn ( move | | { for j in 0 . . 5 { l e t mut data = mutex . l o c k ( ) . unwrap ( ) ; data [ i ] += 1 0 ; } }) ; } thread : : sleep ms (100) ; l e t mutex = a r c . c l o n e ( ) ; l e t data = mutex . l o c k ( ) . unwrap ( ) ; p r i n t l n ! ( ” L i s t : [ { } , { } , { } ] ” , data [ 0 ] , data [ 1 ] , data [ 2 ] ) ; } • Die Mutex-Struktur implementiert einen gemeinhin bekannten Mutex. Durch Aufruf der lock-Funktion wird sichergestellt, dass im die vom Mutex bewachte Ressource nur von dem ausführenden Thread genutzt wird. unwrap macht die Ressource zugänglich. Man beachte, dass in Rust die Ressource nicht explizit wieder freigegeben werden muss. Sobald die Variable data ihre Gültigkeit verliert (und damit auch die Besitzrechte verloren gehen), wird die Ressource automatisch wieder freigegeben. • Die Arc-Struktur (atomic reference counted pointer ) ermöglicht es, den selben Mutex sicher zwischen mehreren Threads zu teilen ermöglicht. Durch die clone Methode des Arc erhält jeder Thread seinen eigenen MutexWert, der jeweils mit den vollen Rechten des Eigentümers genutzt werden kann. 13 7 Abschließende Gedanken Rust setzt durch die Kombination von funktionaler Striktheit und imperativer Vielseitigkeit einen eigenen Akzent in der Landschaft der Programmiersprachen. Die Möglichkeit, durch eine Vielzahl an starker Annahmen die Performanz und Sicherheit des Codes weitgehend sicherzustellen, ist ein überzeugendes Argument für die Entwicklung in Rust. Jedoch steckt die Sprache noch in den Kinderschuhen. Bevor nicht die ersten ernstzunehmenden Anwendungen in Rust vor die Öffentlichkeit treten und als Proof of Concept dienen, fällt es schwer, eine ernstzunehmende Menge an Entwicklern für die Arbeit mit Rust zu begeistern. Das ist nicht die alleinige Aufgabe von Mozilla. Hierfür braucht es risikofreudige, innovative Entwickler, die als Change Agents die Etablierung der Sprache massiv vorantreiben, indem sie die ersten Applikationen hierfür entwerfen. Dann wird auch die Aufmerksamkeit der wissenschaftlichen Welt auf dieses bemerkenswerte Projekt gelenkt und kritische Auseinandersetzungen begonnen - sowohl auf praktischer als auch auf theoretischer Ebene. So können Schlüsse gezogen werden, die auf intensiven und kritischen Analysen der Sprache basieren, gepaart mit dem Aufgreifen der ersten praktischen Erfahrungen. Ich persönlich hoffe, dass ein solcher Diskurs in naher Zukunft begonnen wird. Unabhängig von dem wertenden Ergebnis hinsichtlich der Programmiersprache Rust bereiten wir so den Weg für neue Innovationen im Bereich der funktionalen und der systemnahen Programmierung; ein Feld, dass meiner Ansicht nach Beachtung verdient hat. Ich selbst habe vor, mein nächstes privates Software-Projekt in Rust zu verfassen, da ich von der Grundidee der Sprache überzeugt bin; aber auch, um einen Beitrag zur Verbreitung dieser Sprache zu leisten. Ob Rust eine Zukunft hat, wird sich in den kommenden Monaten abzeichnen. Eines ist sicher: Der Fortschritt dieser Sprache ist und wird spannend bleiben. 14 Literatur [1] ”The Rust Programming Language”(Homepage of the Rust Programming Language) http://www.rust-lang.org/ [2] Avram, Abel (2012-08-03). Ïnterview on Rust, a Systems Programming Language Developed by Mozilla”(Interview mit Graydon Hoare, Initiator der Sprache Rust) http://www.infoq.com/news/2012/08/Interview-Rust [3] ”C++ design goals in the context of Rust”(Vergleich des Designs von C++ und Rust) http://pcwalton.blogspot.de/2010/12/c-design-goals-in-context-ofrust.html [4] ”The Rust Programming Language”(Einführendes Lesewerk in die Programmierung mit und korrekte Nutzung von Rust) http://doc.rust-lang.org/1.0.0-beta.3/book/ [5] Release Notes von Rust https://github.com/rust-lang/rust/blob/master/RELEASES.md [6] Offizielle Github-Organization für Rust https://github.com/rust-lang/ [7] Rust by Example (Tutorial-Website für Rust mit veränderbaren und ausführbaren Codes) http://rustbyexample.com/ Nachdem sich die Programmiersprache Rust noch in einer experimentellen Phase befindet, ist die Beachtung in wissenschaftlichen Publikationen vernachlässigbar gering. Die angeführten Ressourcen wurden bei der Annäherung an die Sprache sowie die Entwicklung dieser Arbeit zu Rate gezogen. Alle Links wurden am 29.04.2015 zuletzt auf Korrektheit und Aktualität geprüft. 15