Eine Systemprogrammiersprache, die die Welt verändert

Einführung in Rust

Einführung in Rust

Eine Systemprogrammiersprache, die die Welt verändert

Einführung in Rust


Rust wurde 2008 als Forschungsexperiment bei Mozilla gestartet mit dem Ziel, eine sichere Systemprogrammiersprache bereitzustellen. Die Sprache wurde 2015 stabilisiert und hat seitdem zwei größere „Editionen“ erhalten, die den Umfang der Sprache erweitert haben. Bis heute wurde Rust von Millionen Entwickler:innen auf der ganzen Welt angenommen. Was also ist Rust? Und was macht Rust so besonders?

In diesem Artikel tauchen wir tief in einige der technischen und sozialen Aspekte ein, die Rust zu einer Sprache machen, die die Landschaft der Programmierung für immer verändert hat.

Es ist nicht einfach, einen einzelnen Grund für die Beliebtheit von Rust herauszustellen. In vielerlei Hinsicht ist es gelungen, großartige Konzepte aus anderen Programmiersprachen zu übernehmen, ohne deren technische Altlasten mitzuschleppen. So erhalten wir die Performance einer Low-level-Programmiersprache, ohne ihre 20 Jahre alten Standardbibliotheken zu übernehmen. Oder wir bekommen die Möglichkeit zur Schnittstellenkomposition, ohne uns Probleme mit Mehrfachvererbung einzuhandeln.

Das Typsystem von Rust ist stark von Haskell und OCaml beeinflusst und bringt zum ersten Mal funktionale Programmierparadigmen in die Systemprogrammierung. In den folgenden Abschnitten möchte ich einige der besten Eigenschaften von Rust hervorheben.

Speichersicherheit in Rust

Die Art und Weise, wie Programme den Anwendungsspeicher verwalten, ist ein zentraler Bestandteil der Softwareentwicklung. Im Allgemeinen gibt es dafür zwei Ansätze.

Manuelle Speicherverwaltung

Bei diesem Ansatz ist der Benutzer der Sprache für die Zuweisung und Freigabe von Speicher verantwortlich. Dies geschieht durch Aufrufe an die grundlegenden Systembibliotheken, beispielsweise mittels malloc() und free(). Dieser Ansatz ermöglicht zwar einige sehr spezifische Optimierungen im Programmcode, birgt aber auch eine Reihe von Problemen.

Was passiert zum Beispiel, wenn ein Teil des Programms einen Speicherbereich freigibt, der von einem anderen Teil noch gebraucht wird? Oder was passiert, wenn zwei Teile des Programms beschließen, denselben Speicherbereich freizugeben? Das kann harmlos sein, kann aber auch zu Programmabstürzen führen, weil ein Speicherbereich, der an derselben Stelle neu zugewiesen wurde, vorzeitig zerstört wird. Diese beiden Szenarien werden „use after free“ und „double free“ genannt.

Speicherverwaltung mit Garbage Collection

Eine gängige Lösung für die Speicherverwaltung, die die Last nicht dem Entwickler aufbürdet, ist die Garbage Collection. Es gibt viele verschiedene Ansätze, wie Garbage Collection funktionieren kann, doch ist die grundlegende Idee einfach: Zusätzlich zum Hauptprogrammcode existiert eine Laufzeitumgebung im Hintergrund, jede Speicherzuweisung wird von dieser Laufzeitumgebung verwaltet, etwa indem sie malloc() und free() für uns aufruft. Anstatt Speicher zuzuweisen, erstellen wir Objekte.

Ein speicherverwaltetes System initialisiert den Speicher für jeden Typ, den der Entwickler nutzen möchte, und verfolgt dann, wie oft dieser Wert in der Codebasis verwendet wird. Erst wenn die letzte Nutzung vorüber ist (z. B. weil der Wert den Anwendungsbereich verlassen hat), kann der zugehörige Speicher wieder freigegeben oder für einen ähnlichen Wert wiederverwendet werden.

Zeiger auf Werte, die nicht mehr existieren, sind zwar immer noch möglich, werden aber vom Speicherverwaltungssystem abgefangen und führen nicht zu ungültigem Speicher. Das Programm kann auf diese Weise immer noch abstürzen, allerdings sind die Auswirkungen dieser Abstürze viel geringer.

Rust verwendet keinen dieser beiden Ansätze und erzeugt stattdessen ein Speicherverwaltungssystem mit statischer Analyse. Alle Speicherbeziehungen werden zur Kompilierzeit überprüft. Während der Laufzeit wird der Speicher nach Bedarf über malloc() und free() im Hintergrund zugewiesen und freigegeben, ohne dass der Benutzer eingreifen muss. Und das Wichtigste: Es gibt keine Laufzeitumgebung! Das Speicherverwaltungssystem muss nicht regelmäßig aktualisiert werden. Alle Aufrufe von malloc() und free() werden zur Kompilierungszeit in den Programmcode eingefügt.

Sichere Nebenläufigkeit

Moderne Hardwarearchitekturen sind in ihrem Anwendungsspektrum immer breiter aufgestellt, aber nicht notwendigerweise schneller als früher. Betrachtet man die Entwicklung der Taktfrequenzen in den letzten 40 Jahren, so ist der eindeutige Trend, dass die Geschwindigkeiten gesunken sind, doch die Menge der gleichzeitigen Operationen, die ein System durchführen kann, deutlich gestiegen ist. Die meisten Endgeräte haben heute mehr als zwei oder sogar vier Kerne.

Um all diese Operationen zu bewältigen, müssen die Anwendungen unter Berücksichtigung von Nebenläufigkeit und Parallelität geschrieben werden. Aber diese Konzepte bringen auch eine große Fehlerquelle in eine Anwendung.

Race Conditions sind Fehler, die auftreten, wenn mehr als zwei gleichzeitige Ausführungsstränge (kurz: Threads) auf eine Weise miteinander interagieren, die der Entwickler der Anwendung nicht vorhergesehen hat. Im mildesten Fall führt das zu ungültigen Ergebnissen einer Operation, im schlimmsten Fall zum Absturz des Programms. Beispielsweise kann der erwähnte „Use after free“-Speicherzugriffsfehler leicht mit mehreren Threads erzeugt werden.

Rust bietet ein Typsystem, das die Grenzen der Nebenläufigkeit von Threads kennt und viele gängige Race Conditions verhindert. Darüber hinaus bietet es eine Reihe von Werkzeugen zur Erstellung von Multi-Thread-Anwendungen, die nebenläufige Systeme so effizient wie möglich ausnutzen.

Embedded

Da Rust nicht auf eine Laufzeitumgebung angewiesen ist, können die erzeugten Binärdateien sehr klein sein. In Verbindung mit den modernen Sprach- und Sicherheitsfunktionen ist das der Grund, warum Rust von Embedded-Entwicklern häufig als Ersatz für fehleranfällige C-Codebasen genutzt wird. Während die Unterstützung für viele Hardware-Boards noch in der Entwicklung ist, kann Rust bereits auf der STM32-Mikrocontrollerfamilie eingesetzt werden. Die Arbeitsgruppe „Rust Embedded“ treibt diese Entwicklungen voran.

Fehlerbehandlung

Rust kennt keine Exceptions, und es gibt nicht einmal einen Nulltyp. Das reduziert die Komplexität im Umgang mit Fehlern deutlich. In Rust existieren sogenannte Panics. Dabei handelt es sich um komplette Programmabstürze, von denen man sich nicht mehr erholen kann. Für eine Bibliothek ist es also sehr verpönt, „in Panik zu geraten“. Stattdessen verwendet Rust ein algebraisches Aufzählungstypsystem, das entweder Erfolg oder Misserfolg im Rückgabetyp einer Funktion ausdrücken kann.

Let mut buf = String::new();
let f = File::open("article.txt")?;
f.read_to_string(&mut buf)?;

Dieser Codeabschnitt erzeugt eine leere Zeichenkette, die als Puffer dient, öffnet dann eine Datei und liest die Datei in den Puffer. Der Operator ? prüft, ob die Operation erfolgreich war. Wenn ja, wird der Inhalt des Funktionsaufrufs (in diesem Fall File) zurückgegeben. Tritt ein Fehler auf, wird er von der aktuellen Funktion zurückgegeben. Wenn das die main-Funktion ist, stürzt das Programm ab.

Pattern Matching

Rust verfügt über eine Pattern-Matching-Syntax, mit der schnell verschiedene Bedingungen eines zurückgegebenen Werts überprüft werden können. Das ähnelt den switch-case-Anweisungen in C++, doch geht das Pattern Matching noch einen Schritt weiter:

let f = match File::open("article.txt") {
  Err(_) => File::create("article.txt"),
  file => file,
}?;

Dieser Code prüft explizit, ob die Operation File::open erfolgreich war. War das nicht der Fall, wird der Typ Err(...) zurückgegeben und es wird stattdessen versucht, eine Datei zu erstellen. War die Operation erfolgreich (d. h., file stellt die Variante Ok(f) dar), wird der Wert durchgereicht. Der ?-Operator nach dem Match-Block wird jeden neuen Fehler, der durch die Erstellung verursacht wurde, wieder an die aufrufende Funktion weiterleiten. Man kann vielleicht schon erkennen, wie dieses System zu einer sehr flexiblen lokalen Fehlerbehandlung führt. Eine Funktion kann auf Bedingungen prüfen, von denen sie weiß, wie sie Fehler möglicherweise selbst beheben kann, während sie andere Fehler in der Kette nach oben an eine Komponente weitergibt, die für die Behandlung dieser Fehler besser geeignet ist.

Oberflächlich betrachtet hört sich das nach der üblichen Funktionsweise einer Exception-Behandlung an. In Wirklichkeit ist das Rust-System jedoch viel eindeutiger als Exceptions, die um eine Funktion herum ausgelöst werden können.

Ownership, Borrowing und Lifetimes

Rust löst viele Probleme der Speicherverwaltung, ohne sich auf Garbage Collection verlassen zu müssen. Das geschieht über die Konzepte von Ownership und Borrowing (Kasten: „Ownership und Borrowing in Rust“). Zusammenfassend lässt sich sagen, dass jedes Datenelement in einem Rust-Programm genau einen einzigen expliziten Scope hat, sei es ein struct oder ein enum, sei es ein Funktionsbereich oder ein anderer Scope im Body einer Funktion (z. B. der Match-Block). Nur der Owner ist dafür verantwortlich, den Speicher eines Typs, der den Scope verlässt, aufzuräumen. Und da es immer nur einen einzigen Owner geben kann, kommt es auch nie zu einem Double-free-Fehler. Aber was ist, wenn man möchte, dass andere Teile der Anwendung diesen Speicher verwenden, ohne dass diese gleich die neuen Owner werden? Und was ist mit User-after-frees?

An dieser Stelle kommen Borrowing und Lifetimes ins Spiel. Jeder Scope, dem ein Teil der Daten gehört, kann diese Daten an einen anderen Scope ausleihen. Das geschieht entweder unveränderlich (was bedeutet, dass der Entleiher die Daten nicht ändern kann) oder veränderlich. Es gibt auch einige Regeln für diese Ausleihen. Zum Beispiel ist ein veränderliches Ausleihen exklusiv. Es können keine anderen veränderbaren oder unveränderbaren Ausleihen gleichzeitig existieren, um Daten-Races zu verhindern.

Schauen wir uns als Beispiel Listing 1 an, um diese Konzepte zu verstehen. Zuerst erstellen wir eine neue Struktur namens Dot. In Rust gibt es keine Klassen, sondern nur Structs und Enums zum Speichern von Daten. In diesem Fall halten wir zwei 64-Bit-Integer mit Vorzeichen fest: x und y.

Als Nächstes deklarieren wir drei Funktionen: inspect(), move() und pacman(). inspect() nimmt ein &Dot, was eine unveränderliche Ausleihe von Dot ist. Das bedeutet, dass inspect sich alle in Dot enthaltenen Werte ansehen, sie aber nicht verändern kann. &Dot wird auch nicht der neue Owner, und daher wird kein Speicher aufgeräumt, nachdem inspect beendet wurde. Die zweite Funktion ist move(). Sie nimmt ein &mut Dot entgegen: eine veränderbare Ausleihe von Dot. Das ist ein exklusives Borgen, das es move() erlaubt, Änderungen an allen in Dot enthaltenen Werten vorzunehmen. Aber auch hier ändern sich keine Besitzverhältnisse. Als Letztes definieren wir eine Funktion pacman(), die die Ownership von Dot übernimmt. Im Rust-Jargon würden wir sagen: „pacman konsumiert Dot“.

Ownership und Borrowing in Rust

Listing 1 zeigt ein einfaches Beispiel zu Ownership und Borrowing in Rust. #[derive(Debug)] ist ein Makro, das den Trait Debug an Dot anhängt, was ermöglicht, eine Debug-Repräsentation auszugeben. Rust verfügt über Verhaltensweisen, die so für jeden neuen Typ abgeleitet werden können, wodurch die Menge an Boilerplate-Code reduziert wird.

Listing 1

 
#[derive(Debug)]
struct Dot {
  x: i64,
  y: i64,
}
 
fn main() {
  let mut dot = Dot { x: 4, y: 2 };
  inspect(&dot);
  move(&mut dot, 3, 3);
  inspect(&dot);
  pacman(dot); // wakka wakka wakka
}
 
fn inspect(dot: &Dot) {
  println!("Inspect: {:?}", dot);
}
 
fn move(dot: &mut Dot, x: i64, y: i64) {
  dot.x = x;
  dot.y = y;
}
 
fn pacman(dot: Dot) {
  println!("Eating delicious {:?}", dot);
}

Das ist indes nur ein einfaches Beispiel. Was passiert jedoch, wenn Daten über einen längeren Zeitraum ausgeliehen oder in anderen Teilen der Anwendung gespeichert werden müssen? Im Fall von inspect() und move() wird die Ausleihe sofort nach dem Aufruf der Funktion zurückgegeben.

Aber Rust kann noch viel mehr. An dieser Stelle kommen Lifetimes ins Spiel. Lifetimes in Rust werden oft missverstanden, und ich hoffe, hier ein paar Dinge aufklären zu können. Schauen wir uns ein kurzes Beispiel für ein struct an.

struct Dot<'num> {
  x: &'num i64,
  y: &'num i64,
}

Wir schreiben eine weitere Definition von Dot. Dieses Mal soll Dot aber nicht selbst Owner seiner Daten sein, sondern sie sich von einem anderen Ort ausleihen. Es handelt sich um unveränderliche Borrows, was bedeutet, dass sie weder von außen noch von Dot selbst geändert werden können. Das Syntaxelement <'num> definiert eine neue Lifetime, eine Annotation für den Rust-Compiler, um ihn anzuweisen, den Lebenszyklus dieses Borrows unter einem bestimmten Namen zu verfolgen. Diese Lifetimes werden später vom Compiler ausgefüllt, je nachdem, wo und wie wir eine Instanz von Dot erstellen. Es ist wichtig zu beachten, dass Lifetimes den Code nur beschreiben können, wie er bereits ist. Sie ändern nicht das Layout des Speichers oder die Art und Weise, wie verschiedene Instanzen von Typen miteinander interagieren.

In gewisser Weise sind Lifetimes Tautologien für die Speicherverwaltung: Sie sind sinnvoll, wenn sie sinnvoll sind. Andernfalls kommt es zu einem Kompilierungsfehler, der fast immer mit einem Lifetime-Problem zusammenhängt, aber nie durch dieses verursacht wird. Kurz gesagt: Es gibt keine Lifetimes, die das Programm „zum Laufen bringen“. Lifetimes können uns jedoch erklären, warum unser Programm nicht funktioniert.

Schauen wir uns ein Beispiel an, bei dem wir Dot erfolglos initialisieren. Dieser Code lässt sich nicht kompilieren, und der Grund dafür liegt darin, wie die Lifetime von Dot<‘num> und die x und y zugewiesenen Werte zusammenwirken.

fn create_dot<'num>() -> Dot<'num> {
  Dot { x: &5, y: &2 }
}

Bei der Erzeugung dieser Instanz initialisieren wir auch zwei Variablen auf dem Funktionsstapel von create_dot(). Aber am Ende dieser Funktion geben wir Dot mit Referenzen auf diesen Stack zurück, ohne die Werte selbst zurückzugeben. Das führt zu einem „hängenden Zeiger“, der Rust daran hindert, unser Programm überhaupt zu kompilieren. Wenn ich an meine Zeit zurückdenke, in der ich, von Python kommend, C lernte, war das vielleicht mein häufigster Fehler. In Rust ist dieser Fehler völlig unmöglich. Um diese Funktion zu fixen, müssen wir x und y als Funktionsparameter hinzufügen. Auf diese Weise können wir Dot mit Referenzen auf einen Ort außerhalb des Funktionsstapels definieren, der vermutlich genauso lange lebt wie die Dot-Instanz.

Das ist natürlich nur eine Einführung in die Konzepte Ownership, Borrowing und Lifetimes, aber ich hoffe, eine Vorstellung davon gegeben zu haben, wie diese Systeme im Prinzip funktionieren. Das Herzstück der Nebenläufigkeit in Rust ist das Ownership- und Borrowing-Modell, das vom Compiler streng erzwungen wird. Dadurch wird es unmöglich, bestimmte Arten von Race Conditions zu erzeugen.

Structs, Enums und Traits

Rust ist keine objektorientierte Sprache, auch wenn es manchmal so aussehen mag. Insbesondere fehlen Laufzeitpolymorphismus, Klassen, Subtypen und Methodenüberladung. Es ist jedoch möglich, mit dem Schlüsselwort impl Methoden auf einem Typ zu implementieren. Rust hat auch Interfaces, sogenannte Traits (die wir bereits als Teil des Debug-Traits kennengelernt haben).

Insgesamt greift Rust einige gängige Muster aus objektorientierten Sprachen auf und legt sie auf Basistypen, um viele der gängigen Programmiertechniken zu ermöglichen, während es gleichzeitig typische Fallstricke der echten Objektorientierung in Low-Level-Sprachen vermeidet.

Structures

Der folgende Code implementiert eine veränderbare Instanzmethode auf Dot (die Version, die Eigentümer ihrer Felder ist und sie nicht nur ausleiht). Der Zugriff auf die Felder von Dot erfolgt über das veränderbare Borrow self.

impl Dot {
  pub fn move(&mut self, x: i64, y: i64) {
    self.x = x;
    self.y = y;
  }
}

Es gibt auch keine speziellen Methoden für Typen. Die Tatsache, dass die meisten Typen eine new()-Funktion haben, ist eine Konvention. Methoden können auch für Structs und Enums implementiert werden.

Die Erweiterung von Strukturen mit zusätzlichen Feldern erfolgt in der Regel durch Komposition, das heißt, die Typen werden in Datenbäumen verschachtelt. Es existiert dann entweder ein einheitliches API im Top-Level-Typ, oder ein Typ wird in einem anderen Typ dereferenziert.

Enums

Rust verfügt über extrem mächtige Enums, die viele verschiedene Arten von Datenausdrücken ermöglichen. Die zuvor vorgestellten Konzepte zur Fehlerbehandlung bauen auf den Enums Result<T, E> und Option<T> auf. Ein Enum kann immer nur eine seiner vielen Varianten sein, hat aber ansonsten keine Grenzen. Schauen wir uns in Listing 2 ein Beispiel für ein einfaches Buchverwaltungssystem an, das über ein Rust-Enum implementiert wurde. (Hinweis: Wer den Code aus Listing 2 verwenden möchte, muss die Dependency zur chrono-Bibliothek einbeziehen, die zeitzonenspezifische Datums- und Zeittypen bereitstellt.)

Listing 2

/// This type represents books in a library
enum BookState {
  /// The book is available in the library
  Available,
  /// The book has been borrowed by someone
  Borrowed { from: Date, to: Date },
  /// The book is otherwise unavailable
  Unavailable,
}
 
impl BookState {
  pub fn check_earliest_availability(&self) -> Option<Date> {
    match self {
      Self::Available => Some(Date::now()),
      Self::Borrowed { to, .. } => Some(to.clone()),
      Self::Unavailable => None,
    }
  }
}

Wie man sieht, können recht komplexe Daten in eine Enum-Variante eingebettet werden. Die Variante Borrowed zum Beispiel liefert uns sowohl das Anfangs- als auch das Enddatum einer Buchausleihe. Es ist natürlich auch möglich, externe oder generische Typen in eine Enum-Variante einzubinden. In diesem Beispiel verwenden wir auch Pattern Matching, um zwischen den verschiedenen Zuständen des Enum zu unterscheiden. Auf diese Weise muss der aufrufende Kontext die interne Logik des Enum nicht verstehen und kann stattdessen einfach das bereitgestellte API verwenden, wie es bei jedem anderen Typ der Fall wäre.

Traits

Rust verfügt über einen Schnittstellenmechanismus namens Traits. Ein Trait ist ein einfaches Funktionsinterface, das standardmäßig keine Funktionsimplementierungen bereitstellen muss. Ein Trait kann für einen bestehenden Typ implementiert werden. Ein bemerkenswerter Unterschied zwischen Traits und z. B. Java-Schnittstellen ist, dass Rust Traits keine eigenen Werte enthalten können und keine Möglichkeit darstellen, Vererbung in die Sprache einzuschleusen.

Schauen wir uns ein weiteres Codebeispiel an, um zu untersuchen, wie Traits funktionieren. Der Trait in Listing 3 bietet eine einzelne Funktion move_by(), die wir für unser Tupel Struct Bicycle implementieren. Von nun an können wir eine Instanz von Bicycle überall dort verwenden, wo ein generischer Parameter von Vehicle erwartet wird, und jede Methode aufrufen, die vom Vehicle Trait bereitgestellt wird.

Listing 3

trait Vehicle {
  fn move_by(&mut self, x: i64, y: i64);
}
 
struct Bicycle(i64, i64);
impl Vehicle for Bicycle {
  fn move_by(&mut self, x: i64, y: i64) {
    self.0 += x;
    self.1 += y;
  }
}

Traits können auch voneinander abhängen, um komplexe Beziehungen zwischen ihnen aufzubauen. Das spielt gut mit dem generischen Typ-system von Rust zusammen, auf das ich nicht im Detail eingehen kann, weil es den Rahmen des Artikels sprengen würde. Kurz gesagt kann man Traits erstellen, die andere Traits voraussetzen, um komplexe Strukturen von erforderlichen Verhaltensweisen in unseren API-Endpunkten aufzubauen:

trait Vehicle: Moves + Capacity { /* ... */ }
pub fn add_vehicle<T: Vehicle>(v: T) -> Result<(), VehicleError> { /* ... */ }

Das Rust-Ökosystem

Wir wollen für einen Moment unser Blickfeld erweitern und das Ökosystem rund um Rust betrachten. Die Basissprache ist bereits ziemlich beeindruckend und bietet sowohl Sicherheiten als auch Annehmlichkeiten in einem Bereich, in dem es früher notorisch schwer war, als Entwickler Fuß zu fassen. Aber ein Teil des Erfolgs von Rust ist auf die Communitystruktur und das Ökosystem von Bibliotheken zurückzuführen, die von passionierten Mitwirkenden geschrieben wurden.

Cargo und crates.io

Ein starkes Alleinstellungsmerkmal von Rust gegenüber anderen Systemprogrammiersprachen ist das Ökosystem von Crates, Cargo und crates.io. Es gibt viele Meinungen zu diesem Thema. Ich glaube zwar nicht unbedingt, dass das aktuelle System perfekt ist. Es bietet aber genügend Verbesserungen gegenüber der Art und Weise, wie Bibliotheken und Abhängigkeiten in C- und C++-Projekten gehandhabt wurden. Der Einfluss des Ökosystems auf die Akzeptanz und die Unterstützung für Entwickler darf also nicht übersehen werden.

Rust-Projekte werden normalerweise mit dem Cargo-Projektmanager erstellt. Ein Cargo-Projekt besteht aus mindestens einer Rust-Datei (entweder main.rs oder lib.rs, je nachdem, ob man eine Anwendung oder eine Bibliothek schreibt) sowie einer Metadatendatei namens Cargo.toml, in der Dinge wie Paketname, Version und Abhängigkeiten festgehalten werden:

[package]
name = "book-tracker"
version = "0.1.0"
 
[dependencies]
chrono = "0.4" 

Außerdem wird nach der ersten Kompilierung eine Cargo.lock-Datei erstellt, in der Hashes von Abhängigkeiten aufgezeichnet werden, um sicherzustellen, dass Netzwerkausfälle oder Angriffe auf die Lieferkette einen Build in Zukunft nicht beeinträchtigen.

Der Einstieg in Cargo ist denkbar einfach und erleichtert die Entwicklung komplexer Anwendungen erheblich. Das mag für Entwickler, die aus Sprachen wie Python oder JavaScript kommen, keine neue Erkenntnis sein, aber in der Welt der Systemprogrammierung ist das von großer Bedeutung. Projekte können dann mit dem Befehl cargo build erstellt oder direkt mit cargo run ausgeführt werden. Die Abhängigkeiten des Projekts werden automatisch heruntergeladen und für die spezifischen Versionen anderer Bibliotheken und des Rust-Compilers kompiliert, die das Projekt verwendet.

Community Governance

Während Rust als Forschungsprojekt bei Mozilla begann und in den Anfangstagen viele der Kernentwickler bei Mozilla arbeiteten, hat sich dies in den letzten Jahren drastisch geändert. Rust wird von einer vielfältigen Community von Developern entwickelt, die in verschiedenen Unternehmen arbeiten und manchmal auch ihre Freizeit in das Projekt einbringen.

Die Art und Weise, wie in dieser Gemeinschaft Entscheidungen getroffen werden, ist auch ein interessantes Beispiel dafür, wie man dezentrale Entscheidungsstrukturen aufbaut. Es gibt zwar ein Kernteam, das viele soziale Interaktionen überwacht, doch wird die eigentliche Implementierung und Gestaltung verschiedener Aspekte der Sprache und des Ökosystems von kleineren Teams mit sehr spezifischen Aufgabenbereichen durchgeführt. So gibt es beispielsweise das Compilerteam, das sich mit dem Compiler befasst, das Libs-Team, das sich mit den zugehörigen Kernbibliotheken befasst, das Sprachteam, das sich mit der Entwicklung neuer Sprachfunktionen befasst, und viele mehr. Nicht alle Teams sind technisch. Das Communityteam zum Beispiel hilft bei der Verwaltung und Organisation von Veranstaltungen und Interaktionen in der Gemeinschaft.

Und die Teams sind noch nicht alles. Es gibt darüber hinaus kleinere Arbeitseinheiten, die sogenannten Working Groups. Das sind lose zusammengesetzte Gruppen, die helfen, einen Teil des Rust-Ökosystems weiterzuentwickeln. Zum Beispiel existieren Working Groups für CLI und async. Jede kleine bis mittelgroße Gruppe von Mitwirkenden, die gemeinsam an einem bestimmten Problembereich arbeiten möchte, kann problemlos eine offizielle Working Group gründen und ihren Einfluss innerhalb der Gemeinschaft erweitern.

Die Entscheidungsfindung erfolgt über einen RFC-Prozess. Dabei kann jeder einen RFC (Request for Comments) erstellen, der dann von der Community beraten und im Laufe der Zeit angenommen oder abgelehnt wird. Dieser Prozess ist zwar nicht perfekt, aber er bedeutet, dass die Macht, Dinge zu ändern, in der Gemeinschaft verwurzelt ist, und nicht von oben bestimmt wird, wie die Dinge zu sein haben.

Fazit

Ich hoffe, dass ich Sie zumindest ein bisschen neugierig auf Rust und den Kontext, in dem es in der Welt der Programmierung existiert, machen konnte. Abschließend möchte ich noch erklären, warum ich diesen Artikel so untertitelt habe, wie ich es getan habe. Ich bin sicher, dass jeder anerkennen wird, dass Rust einige beeindruckende Features unter der Haube hat. Aber warum bin ich der Meinung, Rust sei „eine Systemprogrammiersprache, die die Welt verändert“?

Nun, als Entwickler:innnen sind wir oft mit dem Erbe vergangener Entwürfe, Fehler und Belastungen konfrontiert. Oft wird uns gesagt, entweder von den Technologien oder von den Menschen, mit denen wir zusammenarbeiten, dass die Dinge aus guten Gründen so sind, wie sie sind, dass die Beschränkungen der von uns verwendeten Technologien diesen Technologien inhärent sind. Aber Rust zeigt, dass auch andere Wege möglich sind. Rust ist eine Sprache, die das binäre Entweder/Oder-Denken auflöst: Sie bietet vollständige Speichersicherheit, aber ohne die Kosten einer Laufzeitumgebung; es ist eine leistungsfähige Low-Level-Sprache, aber mit extrem modernen Typsystemfunktionen. Rust existiert jenseits des Verständnisses bestimmter Modelle, die versuchen, die Technologie in einen Zustand zwischen zwei Polen zu zwingen, der willkürlich gewählt wird. Und damit gibt mir Rust viel Hoffnung für die Zukunft der Technologie!

Wenn wir unseren Beruf mit einer ehrlichen Neugierde angehen, anstatt denselben Richtlinien zu folgen, die das Design unserer Werkzeuge in den letzten Jahrzehnten bestimmt haben, können wir Lösungen für Probleme finden, die wir bisher für unmöglich hielten. Und das ist etwas, was im Laufe der Zeit wichtiger werden wird, da die von uns entwickelten Tools und Technologien ausgereifter und für das Funktionieren unserer Gesellschaft immer wichtiger werden.

fey_katharina_sw.tif_fmt1.jpgKatharina Fey ist eine unabhängige Softwareentwicklerin mit Fokus auf verteilte Netzwerke und Routing. Sie arbeitet an einer Mesh-Networking-Plattform namens Irdest und ist außerdem freiberuflich für Unternehmen tätig, die verteilte Systeme in Rust entwickeln. Seit Ende 2017 ist Katharina ein aktives Mitglied der Rust-Community und Teil der CLI-Arbeitsgruppe und des Communityteams.

Katharina Fey

Katharina Fey ist eine unabhängige Softwareentwicklerin mit Fokus auf verteilte Netzwerke und Routing. Sie arbeitet an einer Mesh-Networking-Plattform namens Irdest und ist außerdem freiberuflich für Unternehmen tätig, die verteilte Systeme in Rust entwickeln. Seit Ende 2017 ist Katharina ein aktives Mitglied der Rust-Community und Teil der CLI-Arbeitsgruppe und des Communityteams.


Weitere Artikel zu diesem Thema