Entwicklung: Warum Rust die Antwort auf miese Software und Programmierfehler ist

Rust bringt so viele schlaue Ideen mit, dass die Programmiersprache viele Experten sofort überzeugt hat. Und hoffentlich einer rosigen Zukunft entgegensieht.

In Pocket speichern vorlesen Druckansicht 1234 Kommentare lesen
Entwicklung: Warum Rust die Antwort auf miese Software und Programmierfehler ist

(Bild: Callum Bainbridge / Shutterstock.com)

Lesezeit: 18 Min.
Von
  • Felix von Leitner
Inhaltsverzeichnis

Die 2000er- und 2010er-Jahre sind goldene Zeiten für neue Programmiersprachen. Es gab eine veritable kambrische Explosion der Vielfalt. Darunter etwa neue Versionen altbekannter Sprachen (C++ hatte in der Zeit gleich mehrere inhaltlich große Sprünge in der Evolution) oder einen Trend zu eigenen Sprachen für Unternehmen (Swift von Apple, TypeScript von Microsoft, Go von Google, Kotlin von JetBrains, Rust von Mozilla). Dazu jede Menge ernsthafter neuer Programmiersprachen außerhalb von existierenden Firmen-Ökosystemen, zu Beispiel Elm für Apps in Webbrowsern, Julia für numerische Mathematik, Elixir für die Erlang-VM etc.

Eine Analyse von Felix von Leitner

Felix von Leitner ist Hacker alter Schule. Er bietet mit seiner Firma Code Blau Beratungsdienstleistungen für IT-Sicherheit an, und plaudert gelegentlich auf Konferenzen über seine Arbeit aus dem Nähkästchen. Bekannt ist er auch für sein Blog und den Podcast "Alternativlos". Kernfrage seiner Forschung ist, wieso die Menschheit anscheinend nicht in der Lage ist, vertrauenswürdigen und belastbaren Code zu produzieren. Er ist sich sicher, dass es nicht technische Probleme, sondern soziale und ökonomische Anreize sind, die das verhindern.

Einer der Gründe für diese vielen neuen Sprachen ist, dass die meisten Aspekte von Sprachen wegautomatisiert wurden. Den Parser generiert man sich automatisch aus einer Grammatik, und als Backend kann man LLVM nehmen, wenn man nicht auf die Java-VM zielt oder auf eine andere VM wie die von Erlang.

In diesem Artikel geht es um Rust.

Im Gegensatz zu praktisch allen anderen modernen Programmiersprachen macht Rust nicht Werbung damit, die Produktivität zu steigern. Das macht hellhörig, ist Produktivitätssteigerung doch für den Rest der Branche der heilige Gral, dem man ewig hinterherhechelt. Rust will natürlich auch die Produktivität steigern, aber nicht bloß beim Code-Schreiben, sondern auch die spätere Wartung, das Debugging, und beim Finden und Wegräumen von Sicherheitslücken.

Gut, den Anspruch des sicheren Codes und der reduzierten Wartung hatte auch Java, wo das Finden von Code Smells und das Refactoring heute eine eigene Branche der Tooling-Industrie durchfüttern. Insofern lohnt sich ein Blick darauf, was Rust denn konkret tut, um diese Ziele zu erreichen. Um das in den Kontext zu rücken, müssen wir allerdings ein bisschen ausholen und schauen, was für Ansätze es da schon gab – und warum die nicht funktioniert haben. Letztlich ist "Wie können wir dafür sorgen, dass unsere Programmierer besseren Code schreiben" ja von Anfang an die Kernfrage in der Softwareentwicklung.

Eine frühe Theorie war, dass die Programmiersprachen zu abstrakt waren und man sie näher an die menschliche Sprache heranbringen muss. Dann ist der Übersetzungsabstand geringer, so die Idee. COBOL hat das ausprobiert. Funktioniert hat es nicht. OK, dann müssen wir das vielleicht stärker formalisieren. Wir lassen unsere Programmiersprache wie mathematische Formeln aussehen. APL hat das ausprobiert. Erfolgreich war es nicht.

Dann versuchen wir es mal mit Komplexitätsreduktion. Modularisierung wird uns retten, später war die Modularisierung sogar zwischen Konzepten, nicht bloß zwischen Quellcodedateien, und wir nannten es Objektorientierung und sprachen von Kapselung. Unter dem Strich hat auch das keinen sichereren oder besseren Code produziert.

Das ist über die Jahrzehnte die zentrale Einsicht: Das Umfeld ist nicht statisch. Es ist dynamisch und Markteinflüssen unterworfen. Wenn wir durch einen Eingriff dafür sorgen, dass die Komplexität beherrschbarer wird, dann führt das nicht dazu, dass die Branche die alten Probleme besser löst, sondern dass man größere Probleme angeht oder dass man die eingeplante Zeit pro Problem reduziert. Das Ergebnis ist, dass die Position auf der Pfusch-Achse konstant bleibt. Selbst hocherfolgreiche Konzepte wie starke Typisierung, Modularisierung, Abstraktion und Kapselung bei objektorientierten Programmiersprachen haben unter dem Strich sehr geholfen, aber die Systeme sind am Ende nicht besser geworden, nur viel größer (und sehr viel langsamer).

Rust kommt von Mozilla, den Leuten hinter dem Firefox-Browser. Der Firefox-Browser ist sozusagen der Worst Case für traditionelle Softwareentwicklung. Es ist eine vergleichsweise riesige Software und trotz Anwendung aller oben genannten Konzepte eine beständige Quelle von Sicherheitslücken. Die Release-Abstände wurden über die Jahre immer kleiner, aber ein Release ohne kritische Sicherheitslücken war nicht darunter. Die Mozilla-Leute spüren also am eigenen Leib, dass die bisherigen Ansätze nicht hinreichen, und sie haben sich hingesetzt und das Problem analysiert.

Es gibt in der Softwareentwicklung mehrere fundamentale Probleme und Hindernisse, und keines davon ist technisch.

Programmierer arbeiten nicht mit Formeln, sondern mit Annahmen und Heuristiken. Man hat Zugriff auf eine mehr oder weniger riesige Bibliothek aus fertigen Lösungen für Detailprobleme und stöpselt aus denen das Gesamtsystem zusammen. Eine rigorose Analyse, ob die Annahmen der zusammengebastelten Komponenten überhaupt gleichzeitig erfüllt sein können, macht niemand. Im Allgemeinen liegen die Annahmen der Komponenten auch gar nicht explizit niedergeschrieben vor.

Softwareentwicklung ist in der Praxis daher immer eine Iteration. Man stöpselt etwas zusammen, dann lässt man den Compiler bauen, wenn man eine Testsuite hat, lässt man die laufen, und dann shippt man. Wenn auf dem Weg Fehler auftreten und noch Zeit übrig ist, dann geht man noch mal zurück und ändert etwas. Von außen erinnert das ein bisschen an Machine Learning, und es hat damit den Aspekt gemein, dass das Ergebnis eher gelernt als konstruiert ist und man gar nicht wirklich sagen kann, warum das jetzt gerade beziehungsweise unter welchen Rahmenbedingungen es auch für andere Eingaben funktioniert. Man hat bloß die Hybris des Programmierers als Grundlage, der selbstverständlich annimmt, die eigene Software sei praktisch fehlerfrei und funktioniere in allen Fällen, außer vielleicht, wenn der Anwender sie falsch einsetzt.

Programmieren selektiert wie Bergwandern nach Leuten mit unrealistischer Selbsteinschätzung ("klar kann ich das!"), die sich fortlaufend über den Fortschritt des Projektes selbst belügen können ("Wir sind so gut wie da! Nur noch um die eine Ecke da vorne, da muss es dann sein!"). Wenn man das ohne Selbstbelügen und mit realistischem Projektmanagement betreiben würde, bekäme man keinen einzigen Zuschlag, weil der Rest des Feldes mit Leuten gefüllt ist, die glauben, dass sie das Geforderte mal eben am Wochenende herunterhacken können.

Wenn man die Hybris der Programmierer mit dem ewigen Marktdruck kombiniert, werden in vielen Fällen Dinge aufgeschoben. Ja klar, das könnte man hier auch ordentlich machen, aber das wird in der Praxis schon nicht so schlimm sein, und wenn doch, dann kann ich ja zurückkommen und nachbessern. Programmieren ist in der Praxis eher ein Optimierungsproblem (was ist der geringste Aufwand, den ich treiben muss, damit der Kunde mir das abnimmt) als eine konstruktive Ingenieurskunst. Schlimmer noch: Wenn man einen Programmierer findet, der alles ordentlich machen will, dann ist der im Markt nicht konkurrenzfähig gegen die ganzen Abkürzungen der Pfuscher der Konkurrenz.

Der Ansatz von C und C++ war und ist, dass man viele Dinge einfach als "das darfst du halt nicht machen" deklariert. Man schreibt dann in den Standard "das ist implementationsspezifisch" oder "das ist undefiniertes Verhalten", und dann kann man dem Programmierer sagen, er hätte halt kein undefiniertes Verhalten produzieren sollen: Man hat sich in seinem theoretischen Elfenbeinturm die Hände reingewaschen von dem Schmutz der Praxis. Ein illustrierendes Beispiel dafür ist std::vector in C++, wo man mit vektor[n] indiziert auf Elemente zugreifen kann. Der Vektor weiß, wie viele Elemente er hat. Der Vektor liefert die Implementation von operator[]. Aber der Standard sagt, der Vektor muss nicht prüfen, ob der Index gültig ist. Wenn er das nicht ist, ist das in der Theorie undefiniertes Verhalten und in der Praxis eine Sicherheitslücke. Das C++-Komitee findet das schon OK so. Wer den Index geprüft haben will, kann ja statt vektor[n] einfach vektor.at(n) schreiben.

Das ist absolut konsistent für C und C++. In C prüft der indizierte Array-Zugriff auch nichts, und C++ hat das einfach übernommen.

Es gibt einen Weg, das sicher zu machen, aber der ist nicht Default und er ist mit mehr Tippaufwand verbunden und sieht weniger idiomatisch aus. Natürlich macht das in der Praxis niemand.

Auch das ist absolut konsistent über die Zeit. Wenn man in C einen Integerwert haben will, kann man "int" nehmen. Dann ist die Länge plattformabhängig. Könnte 16 Bit sein oder 32 Bit oder 64 Bit. Wer 32 Bit haben will, nimmt int32_t. In der Praxis sieht man wenig überraschend überall int und nicht int32_t. Das ist weniger Tippaufwand, und der Programmierer ist sich ja eh sicher, dass er schon weiß, was er tut. Genau wie im Autoverkehr immer alle glauben, Autounfälle würden nur den anderen passieren, und man selbst sei ein überdurchschnittlich begabter Autofahrer.

Schlimmer noch: Wenn man in C sagen will, dass ein Wert nur gelesen und nicht geschrieben werden darf, dann muss man "const" dranschreiben. Aus "int" wird dann "const int". Der Tippaufwand hat sich verdreifacht! Wenig überraschend ist alter C++-Code voll von nicht als const deklarierten Werten, die aber eigentlich const gemeint waren oder hätten sein können. Der Compiler kann aber nur meckern, wenn man gesagt hat, dass das als const gemeint war.

Im Maschinenbau kennt man das Konzept von "fail safe". Man nimmt an, dass jede Komponente irgendwann ausfallen kann, und baut das Gesamtgerät so, dass es dann keinen rauchenden Krater hinterlässt, sondern eine heruntergefahrene Maschine. In Zügen gibt es zum Beispiel einen "Totmannschalter". Wenn der Zugführer einen Herzinfarkt hat und am Arbeitsplatz tot umkippt, dann kommt der Zug automatisch zum Stehen und rast nicht unkontrolliert durch die Gegend.