Die Programmiersprache Rust

Werbung
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
Herunterladen