Willkommen zu unserem zweiten Themenmonat: Mechanical March. In diesem Monat konzentrieren wir uns auf Systemsprachen, also Sprachen, die zu Maschinencode kompiliert werden.
Dies ist teils Blogbeitrag, teils Transkript des Mechanical-March-Videos. Ich gebe dir eine kurze Einführung in den Monat, und danach schauen wir uns die Systemsprachen etwas genauer an: ihre Entwicklung im Laufe der Geschichte, die Vor- und Nachteile des Kompilierens zu Maschinencode und jeden der vorgestellten Sprachen kurz. Erik ist wieder dabei und übernimmt in der zweiten Hälfte den Großteil des Redens. Aber ich fange mit ein paar praktischen Informationen an.
Also zuerst die vorgestellten Sprachen in diesem Monat. Das sind C, C++, D, Go, Nim, Rust, V und Zig. Um das Mechanical-March-Abzeichen zu bekommen, musst du fünf Übungen in einer dieser Sprachen lösen. Unser Go-Track hat einen der besten Lehrpläne auf Exercism, also kann ich dir nur empfehlen, ihn auszuprobieren. Wir bei Exercism sind außerdem große Fans von Nim, weil es eine relativ einfache Sprache für den Einstieg ist und sich sehr leicht schreiben lässt. Also probier sie auf jeden Fall auch aus.
Wir haben außerdem fünf vorgestellte Übungen, die du ausprobieren kannst:
- linked-list oder simple-linked-list (je nach Sprache): Speicher allokieren und freigeben, Zeiger
- secret-handshake: Bitoperationen
- pangram: for-Schleifen, Strings und Zeichen
- sieve: Arrays, for-Schleifen
- binary-search: Arrays, Schleifen
Es gibt ein neues Abzeichen, das ich im Update-Video vor ein paar Tagen angekündigt habe. Es ist dafür da, die fünf vorgestellten Übungen in den thematischen Sprachen zu lösen. Um dieses Abzeichen zu bekommen, musst du alle diese Übungen in einer Systemsprache irgendwann im Laufe des Jahres abschließen.
Bei uns köcheln außerdem viele spannende Dinge vor sich hin: Interviews mit ein paar Leuten aus dem Go-Core-Team, hoffentlich auch aus dem Rust-Team und einigen der anderen Sprachen. Außerdem werden wir den ganzen Monat über viel live streamen. Und bald gibt es auch Mechanical-March-Swag!
Also lass uns etwas tiefer in die technische Seite eintauchen.
Wofür werden diese Sprachen verwendet?
Nun, wir setzen sie bei Exercism an verschiedenen Stellen ein. Unser CLI-Tool ist in Go geschrieben, unser internes Track-Verwaltungstool namens configlet ist in Nim geschrieben, und die wichtige Bibliothek, die die Codezeilen in deinen Lösungen zählt, ist in Rust geschrieben. Erik, warum haben wir diese Sprachen für diese Teile des Toolings gewählt?
CLI:
- Ich glaube, wir haben Go genommen, weil Katrina damit am vertrautesten war.
- Go eignet sich hervorragend für diese eher kleinen Kommandozeilen-Tools.
- Go-Code ist relativ geradlinig, was Beiträge einfacher macht.
- Go-Binärdateien lassen sich leicht ausrollen, weil sie keine Laufzeitumgebung benötigen.
- Go macht Cross-Kompilierung gut.
Nim
- Nim hat die meisten der gleichen Vorteile wie Go.
- Wir hatten nicht viele Leute, die Go konnten und das CLI-Tool pflegen konnten, deshalb sind wir zu Nim gewechselt.
Wo sonst würdest du erwarten, diese Sprachen im Einsatz zu sehen?
- Überall dort, wo Leistung wichtig ist (Treiber, Spiele, Betriebssysteme, Build-Systeme/Compiler)
- Überall dort, wo Ressourcen begrenzt sind (z. B. eingebettete Software)
- Alles, was sehr portabel sein muss, also auf vielen verschiedenen Plattformen läuft
Was ist Maschinencode?
Wie ich schon sagte, zeichnen sich die Sprachen im Mechanical March dadurch aus, dass sie zu Maschinencode kompilieren. Kannst du kurz erklären, was Maschinencode ist und was im Gegensatz dazu Bytecode ist?
- Maschinencode ist Code, der direkt auf der Maschine ausgeführt werden kann.
- Bytecode dagegen braucht anderen Code, der den Bytecode interpretiert bzw. ihn zu Maschinencode kompiliert. Bytecode erfordert also einen Zwischenschritt, bevor er ausgeführt werden kann.
Was sind die Vor- und Nachteile von Maschinencode gegenüber Bytecode?
Vorteile:
- Schnellerer Start (kein JIT-Kompilierungsschritt)
- Geringerer Speicherbedarf (keine Laufzeitumgebung, die geladen wird, kein Bytecode im Speicher, ideal für eingebettete Systeme)
- Auf der Zielmaschine muss keine Laufzeitumgebung installiert werden (wichtig, um Docker-Container klein zu halten)
Nachteile:
- Nicht portabel. Bytecode ist portabel, kompilierter Maschinencode ist aber plattformspezifisch.
- Keine fortgeschrittenen Optimierungen wie profilgesteuerte Optimierung möglich (dabei wird nach einer Weile der Ausführung bestimmt, wie der (Byte-)Code am besten kompiliert wird).
Hinweis: Hybride Ansätze sind möglich, bei denen die Sprache zu Bytecode kompiliert und dann ein anderes Tool verwendet wird, um diesen Bytecode zu Maschinencode zu kompilieren.
Die Entwicklung der Systemprogrammierung
Okay. Schauen wir uns die Entwicklung einiger der Sprachen in diesem Monat etwas genauer an. Beginnen wir zuerst bei C und damit, wie daraus C++ entstanden ist. Erzähl uns ein wenig über diese Sprachen.
C ist eine sehr Low-Level-Sprache. Es fühlt sich an, als wärst du nur knapp über dem Maschinencode. Das macht sie sehr mächtig und hochgradig optimierbar, aber auch etwas fehleranfällig (z. B. Nullzeiger-Ausnahmen und Pufferüberläufe). Die Speicherverwaltung ist vollständig manuell und damit Sache der Programmiererin bzw. des Programmierers, was zu Fehlern und/oder Speicherlecks führen kann. C++ ist wie C, aber mit Unterstützung für objektorientierte Programmierung. Es ist noch immer recht low-level und verlangt von dir manuelle Speicherverwaltung. Sowohl C als auch C++ erlauben es dir, Inline-Assembler (ASM) zu schreiben!
Und was ist mit den neueren Systemsprachen? Wie haben sie sich entwickelt?
Alle modernen Systemprogrammiersprachen unterstützen automatische Speicherverwaltung, sei es über Referenzzählung, einen Garbage Collector oder einen anderen Mechanismus.
Frühe Systemprogrammiersprachen unterstützen alle Nullzeiger (von Tony Hoare als sein „Milliarden-Dollar-Fehler“ bezeichnet). Sie sind berüchtigt dafür, zu Laufzeitfehlern und Sicherheitslücken zu führen. Viele moderne Sprachen verzichten auf null oder machen es zumindest mühsam, es zu verwenden.
Eine weitere Veränderung ist der Wandel davon, dass veränderbare Werte die Voreinstellung sind, hin zu standardmäßig unveränderbaren Werten. So sind in Rust und Vlang Werte standardmäßig unveränderbar, und man muss sich für Veränderbarkeit entscheiden.
Alle neueren Sprachen unterstützen die Interoperabilität mit C (oder C++), weil so viel Code in diesen Sprachen geschrieben wurde.
Interessant ist auch, dass einige der neueren Sprachen nicht direkt zu Maschinencode kompilieren, sondern dafür andere Tools verwenden. Rust und Zig nutzen zum Beispiel LLVM, während man bei Nim eine Vielzahl von Compilern verwenden kann. Das nennt man Transpiling.
Und was ist mit Dingen wie Makros und Metaprogrammierung?
Bei Makros bzw. Metaprogrammierung gibt es eine interessante Spaltung. Makros in C/C++ sind mächtig, haben aber einen etwas zweifelhaften Ruf, schwer handhabbar zu sein. Rust, Nim und D bieten alle mächtige Metaprogrammierung, aber auf eine viel schönere Art. Umgekehrt erwähnen VLang und Zig beide ausdrücklich, dass es keine Makros gibt, als Feature ihrer Sprache, und Go hat mit go generate einen anderen Ansatz.
Systemsprachen haben den Ruf, ziemlich low-level zu sein. Stimmt das noch?
Die neueren Sprachen arbeiten alle auf höheren Abstraktionsebenen als C/C++. Rust, D und Nim erlauben zum Beispiel auch eine sehr funktionale Art, Code zu schreiben. Nim und D kennen sogar das Konzept „reiner“ Funktionen.
Einführung in die Sprachen des Monats
Es wäre gut, jede der Sprachen der Reihe nach anzuschauen. Alle Sprachen haben Gemeinsamkeiten: Sie sind alle stark und statisch typisiert. Aber schauen wir, wie sie sich unterscheiden. Fangen wir mit C an?
C
- Entwickelt von Dennis Ritchie
- Eine der ältesten und wahrscheinlich meistgenutzten Sprachen der Welt
- Jede Menge Software ist in C geschrieben, etwa Unix und Linux
- Sehr einflussreich (man denke nur daran, dass es so etwas wie C-ähnliche Sprachen gibt)
- Manuelle Speicherverwaltung
- Sehr performant (nah an der Hardware)
- Läuft überall
- Perfekt für eingebettete Systeme
- Eine recht kleine Sprache
C++
- Entwickelt von Bjarne Stroustrup
- Nachfolger von C, aber mit zusätzlicher Objektorientierung (C mit Klassen)
- Hat objektorientierte Programmierung mit populär gemacht
- Mehr High-Level-Sprachfeatures als C
- Unterstützt generische Programmierung über Templates
- Modulunterstützung über Namespaces
- Viele Spiele(-Engines) sind in C++ geschrieben, ebenso große Teile von Windows
- Manuelle Speicherverwaltung
- Entwickelt sich weiter, regelmäßig kommen neue Features hinzu (große Spezifikation)
D
- Entwickelt von Walter Bright, später kam Andrei Alexandrescu dazu
- Ursprünglich als überarbeitetes C++ gedacht (das aus dessen „Fehlern“ lernt), ließ sie sich von vielen anderen Sprachen inspirieren
- Multiparadigmatisch, unterstützt imperative, objektorientierte und funktionale Programmierung
- Einfache Interoperabilität mit C/C++
- Uniform Function Call Syntax
- Compile Time Function Evaluation (z. B. das Generieren einer Regex-Zustandsmaschine zur Kompilierzeit)
- Unterstützt funktionale Programmierung und „reine“ Funktionen
- Viele Sicherheitsfeatures
- Speichersicherheit über @safe
- Contracts (Vor-/Nachbedingungen, Invarianten)
- Reine Funktionen
- Fokus auf Unit-Tests, wobei die Tests neben dem Quellcode stehen, den sie testen (das erforderte eine Ausnahme auf der Exercism-Website :))
- Unicode
Rust
- Entwickelt von Graydon Hoare, einem Mitarbeiter von Mozilla Research, später offiziell von Mozilla übernommen und heute Teil der Rust Foundation
- Multiparadigmatisch, unterstützt OOP (aber opinionated, z. B. keine Vererbung), imperative und funktionale Programmierung (Option/Result-Typen, Pattern Matching)
- Viele neue Tools sind in Rust geschrieben (z. B. SWC, aber auch Gleam, und als zweite unterstützte Sprache im Linux-Kernel; wir sind für den Codezeilen-Zähler davon abhängig)
- Fokus auf Zuverlässigkeit und Performance
- Baut auf LLVM auf
- Beliebteste Sprache in der StackOverflow-Umfrage der letzten 7 Jahre
- Schnell, teils wegen des minimalen Kerns und der minimalen Standardbibliothek
- Sicher, sowohl Speichersicherheit als auch Thread-Sicherheit durch Ownership und Lifetimes, standardmäßig unveränderbar
- Mächtiges Typsystem, das viele Fehler schon zur Kompilierzeit abfängt (besonders speicherbezogene). Der Compiler gibt wirklich hilfreiche Fehlermeldungen aus
- Alles inklusive: Compiler, Build-Tool, Formatter, Paketmanager, IDE-Integrationen
- Wunderbare Dokumentation (es gibt sogar ein riesiges Dokument darüber, wie der Compiler funktioniert)
- Portabel: kompiliert zu einer einzigen, statischen Binärdatei und erfordert keine installierte Laufzeitumgebung
- Einfache Interoperabilität mit C-Code
- Zero-Cost-Abstractions
- Furchtlose Nebenläufigkeit
- Makros
Nim
- Entwickelt von Andreas Rumpf (ursprünglich Nimrod genannt)
- Syntax von Python inspiriert
- Multiparadigmatisch
- Wird bei Exercism in configlet verwendet
- Tolle Performance: Iteratoren ohne Overhead, Bevorzugung der Stack-Allokation für wertbasierte Typen
- Modernes, ausdrucksstarkes Typsystem: Typinferenz, Tupel, Generics, Summentypen, async/await
- Garbage Collection, aber mit Unterstützung für deterministische Speicherverwaltung (mehrere Optionen zur Speicherverwaltung)
- Ausführung von Code zur Kompilierzeit
- Uniform Call Syntax
- Makros: die Sprache lässt sich leicht erweitern
- Effects-System: Seiteneffekte werden im Typsystem kodiert
Go
- Entwickelt von Robert Griesemer, Rob Pike und Ken Thompson bei Google
- Größtenteils imperativ/prozedural, unterstützt einen objektorientierten Ansatz (aber ohne Vererbung)
- Wird in großen Projekten wie Docker und Kubernetes verwendet. Eignet sich auch hervorragend für Backends und Kommandozeilen-Tools (z. B. esbuild)
- Will einfach genug sein, um es im Kopf zu behalten (wenig Syntax)
- Speichersicherheit durch Garbage Collector
- Schnell: schnelle Kompilierung, schnelles Testen und schnelle Laufzeit. Eingebaute Unterstützung für das Schreiben von Benchmarks!
- Opinionated: viel Aufwand, um den Stil von Go-Code zu beeinflussen: wenig Syntax, Code-Formatierung über go fmt, Tooling zur Prüfung von Code auf idiomatische Verwendung, Fehler bei Dingen, die in anderen Sprachen nur Warnungen sind (z. B. unbenutzte Variablen), Dokumentation mit aufgelisteten Go-Idiomen
- Leichtgewichtiges Typsystem, das Go sehr flexibel macht (unterstützt Typinferenz)
- Portabel: kompiliert zu einer einzigen, statischen Binärdatei und erfordert keine installierte Laufzeitumgebung. Einfache Cross-Kompilierung. Nebenläufigkeit über Goroutines (leichtgewichtig) und Kommunikation über Channels
- Strukturelle Typisierung über Interfaces (ähnlich wie Duck Typing, aber statisch geprüft)
- Fehlerbehandlung: Die Sprache ermutigt dazu, Fehler zu prüfen und zu behandeln
VLang
- Entwickelt von Alexander Medvednikov und Delyan Angelov
- Von Go inspiriert:
- Dieselbe Strategie „nur einen Weg, Dinge zu tun“
- Dieselbe Strategie „minimale Syntax“
- Coroutines
- Anders als Go:
- Kein nil/null, stattdessen wird ein Ergebnistyp verwendet
- Standardmäßig unveränderbar
- Summentypen (funktional)
- String-Interpolation
- Kleinere Laufzeitumgebung/Binärdateien
- Pattern Matching
- Arbeitet daran, GC optional zu machen (autofree)
- Zero-Cost-Interoperabilität mit C
- Dokumentation aus dem Code generieren
- Schneller Compiler, der wenig Speicher verwendet
Zig
- Entwickelt von Andrew Kelley
- Recht wenig Syntax (PEG-Grammatikdatei mit 500 Zeilen)
- Codeausführung und Reflexion zur Kompilierzeit
- Will „offensichtlich“ sein, kein versteckter Kontrollfluss, keine versteckten Allokationen, keine Makros/Metaprogrammierung
- Manuelle Speicherallokation
- Unterstützt verschiedene Allokatoren
- Standardbibliotheksfunktionen, die Speicher allokieren, haben einen Allokator-Parameter
- Das Test-Framework kann Speicherlecks erkennen
- Sicherheit:
- Fehler sind Werte und müssen behandelt werden
- Kein null, verwendet einen optionalen Typ
- Tests können in derselben Datei wie der Quellcode geschrieben werden (wie bei D)
- Verwendet LLVM als Backend
- Kann C/C++-Code kompilieren
- Einfache Interoperabilität mit C
- Einfache Cross-Kompilierung
Fazit
Wir hören hier auf, denn ich stelle mir vor, dass bei allen die Köpfe inzwischen ziemlich voll sind.
Hoffentlich war das eine nützliche und unterhaltsame Einführung in die vorgestellten Sprachen dieses Monats. Ich hoffe, du hast viel Spaß beim Erkunden dieser Sprachen. Erik und ich würden beide gerne hören, welche du wählst und wie du sie findest, also poste gerne in den Kommentaren oder im Forum!
Danke fürs Zuschauen!