Bevor wir uns über Ownership, Lifetimes oder Traits unterhalten, lass uns die Werkzeugkette in Bewegung sehen: ein winziges Programm schreiben, es übersetzen, ausführen, das Binary inspizieren und verstehen, was zwischen fn main() und dem ausgeführten Binary tatsächlich passiert. Wir machen das zweimal — einmal mit dem nackten Compiler rustc, einmal mit Cargo — und vergleichen die Erfahrung. Wenn du am Ende dieses Artikels den Unterschied zwischen Debug- und Release-Build kennst und weißt, wo dein Binary landet, hast du das Fundament für alles, was folgt.
Variante 1: Ohne Cargo, nur rustc
Lass uns mit dem nackten Compiler beginnen — nicht weil es der idiomatische Weg wäre, sondern weil du dadurch siehst, was Cargo später für dich automatisiert.
Erstelle irgendwo eine Datei hello.rs mit folgendem Inhalt:
fn main() {
println!("Hallo, Welt!");
}Im selben Verzeichnis im Terminal:
rustc hello.rs
./hello # auf Windows: .\hello.exeAusgabe:
Hallo, Welt!Im Verzeichnis liegt jetzt eine Binary namens hello (bzw. hello.exe). Sie ist statisch gelinkt gegen die Rust-Standard-Bibliothek, dynamisch gegen libc — das ist der Standard auf Linux und macOS. Auf macOS sind das ungefähr 400 KB, auf Linux 4 MB (statisch eingebundenes std macht den Unterschied). Das ist die Default-Größe eines unoptimierten Debug-Builds.
Was hat rustc da gemacht?
Sehr verkürzt:
- Parsing —
hello.rswird zu einem abstrakten Syntaxbaum (AST). - Name Resolution — alle Bezeichner werden ihren Definitionen zugeordnet.
- Type Checking — der Typ-Checker verifiziert, dass
println!mit den übergebenen Argumenten kompatibel ist. - Borrow Checking — der Borrow Checker prüft Ownership-, Reference- und Lifetime-Regeln. (In
hello.rsgibt's da wenig zu prüfen.) - MIR-Generierung — Mid-level Intermediate Representation, Rusts eigene Zwischensprache.
- LLVM-Codegen — MIR wird zu LLVM IR übersetzt; LLVM optimiert und erzeugt Maschinencode.
- Linking — der System-Linker (
ld/link.exe) bindet alles zu einem ausführbaren Binary.
Drei der sieben Schritte sind reine Rust-Compiler-Arbeit (Parser, Typchecker, Borrow Checker). Drei sind allgemeine Compiler-Architektur (MIR, LLVM, Linker). Schritt 4 — Borrow Checking — ist die Stelle, an der Rust sich von praktisch allen anderen Compilern unterscheidet.
Variante 2: Mit Cargo
In der Praxis ruft niemand rustc direkt auf. Stattdessen nutzt du Cargo, das alles drumherum verwaltet — Dependencies, Builds, Tests, Doku, Release-Profile.
cargo new hello
cd hello
cargo runAusgabe:
Compiling hello v0.1.0 (/Users/du/hello)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/hello`
Hallo, Welt!Was Cargo dir hier abgenommen hat:
- Verzeichnisstruktur angelegt:
Cargo.toml,Cargo.lock,src/main.rs,.git-Repo. - rustc aufgerufen mit den richtigen Argumenten.
- Build-Artefakte in
target/debug/abgelegt — beim nächstencargo runwird der Build incrementell wiederverwendet. - Binary ausgeführt ohne dass du den Pfad kennen musst.
Die generierten Dateien
hello/
├── Cargo.toml ← Projekt-Metadaten und Dependencies
├── Cargo.lock ← Exakte Versionen aller Crates
├── .gitignore
└── src/
└── main.rs ← EinstiegspunktCargo.toml enthält am Anfang nur das Nötigste:
[package]
name = "hello"
version = "0.1.0"
edition = "2024"
[dependencies]src/main.rs enthält das Hello-World-Programm — fast identisch zu dem, was wir oben von Hand geschrieben haben.
Anatomie von main.rs
Schauen wir uns die fünf Zeilen genau an:
fn main() { // Einstiegspunkt — muss main heißen
println!("Hallo, Welt!"); // Makro-Aufruf (siehe !)
}Drei Dinge sind hier konzeptuell wichtig:
fnist das Schlüsselwort für eine Funktion. Klein geschrieben, kurz — Rust-Syntax bevorzugt Knappheit.mainist als Funktionsname konventionell, aber gleichzeitig zwingend: Wenn dein Crate ein Binary ist (alsosrc/main.rshat), erwartet rustc eine Funktionmainmit dieser Signatur. Sie kann auchfn main() -> Result<(), Box<dyn Error>>haben — dazu später mehr.println!mit dem!ist ein Makro, kein Funktionsaufruf. Der Unterschied: Makros expandieren zur Compile-Zeit zu echtem Code. Beiprintln!wird der Format-String zur Bauzeit geprüft — vergisst du ein Argument, kompiliert dein Code nicht.
Das ! ist Rusts Konvention, jedes Makro sichtbar zu machen. Gewöhne dich daran: vec!, format!, assert_eq!, dbg!, panic!, println!, eprintln!, print!, write! — alles Makros.
Was passiert mit println! intern?
Das Makro expandiert zu in etwa Folgendem (sehr vereinfacht):
use std::io::Write;
let stdout = std::io::stdout();
let mut handle = stdout.lock();
writeln!(handle, "Hallo, Welt!").unwrap();Du kannst die echte Expansion mit cargo expand (aus dem Crate cargo-expand) ansehen — sehr lehrreich, sobald wir bei eigenen Makros sind.
Debug- vs. Release-Build
Standardmäßig baut Cargo im Debug-Profil. Optimierungen sind aus, Debug-Symbole sind drin, Builds sind schnell, der Code ist langsam.
cargo build # Debug-Build → target/debug/hello
cargo build --release # Release-Build → target/release/hello
cargo run --release # Release direkt ausführenDer Unterschied ist drastisch. Ein einfacher Iterator-Loop kann im Release-Build zehn- bis fünfzig-mal schneller laufen als im Debug-Build. Das liegt daran, dass viele Rust-Idiome (map, filter, iter()) auf aggressive Inlining-Optimierungen angewiesen sind, die im Debug-Profil bewusst aus sind.
| Aspekt | cargo build | cargo build --release |
|---|---|---|
| Optimierungslevel | opt-level = 0 | opt-level = 3 |
| Debug-Info | debug = true | debug = false (außer du änderst es) |
| Build-Zeit | schnell | langsam |
| Laufzeit | langsam | optimiert, oft 10×+ schneller |
| Output-Pfad | target/debug/ | target/release/ |
| Wann nutzen | Entwickeln, Tests | Benchmarken, Produktion |
Praxis-Regel: Performance-Aussagen niemals aus dem Debug-Build ableiten. „Mein Rust-Code ist langsamer als Python" ist fast immer ein Debug-Build, der gegen optimiertes Python (mit C-Extensions) verglichen wird.
cargo run vs. cargo build vs. cargo check
Drei Kommandos, die du täglich nutzt:
cargo check— typcheckt und borrow-checkt deinen Code, erzeugt aber keine Binary. Schnellster Weg, um zu prüfen „kompiliert das überhaupt?". Faktor 3–10 schneller als ein voller Build.cargo build— vollständiger Build mit Linking. Das Binary landet intarget/debug/(odertarget/release/mit--release).cargo run—cargo build+ sofortiges Ausführen des resultierenden Binaries. Mit Argumenten:cargo run -- arg1 arg2(das--trennt Cargo-Args von Programm-Args).
Faustregel im Entwicklungs-Workflow:
- Beim Schreiben:
cargo checkin einem Watch-Loop (cargo watch -x checkaus dem Cratecargo-watch). - Bei Tests:
cargo test. - Vor Commit:
cargo clippyundcargo fmt. - Zum Ausprobieren:
cargo run. - Zum Veröffentlichen:
cargo build --release.
Erste Edits
Ändere src/main.rs zu folgendem Code und führe cargo run aus:
fn main() {
let name = "Welt";
println!("Hallo, {name}!");
let summe = addiere(3, 4);
println!("3 + 4 = {summe}");
}
fn addiere(a: i32, b: i32) -> i32 {
a + b
}Hallo, Welt!
3 + 4 = 7Hier sind drei neue Konzepte gleichzeitig in Aktion:
let name = "Welt";— eine Bindung an einen&'static str. Type Inference wählt den Typ.{name}im Format-String — Rust unterstützt seit 2021 das direkte Einsetzen von Variablen.fn addiere(a: i32, b: i32) -> i32— eine zweite Funktion. Typen vor jedem Parameter, Rückgabetyp nach->. Letzte Expression ohne Semikolon ist der Rückgabewert.
Wenn du jetzt das Semikolon hinter a + b setzt — a + b; — kompiliert es nicht mehr, weil dann nichts zurückgegeben wird. rustc beschwert sich freundlich:
error[E0308]: mismatched types
--> src/main.rs:9:33
|
9 | fn addiere(a: i32, b: i32) -> i32 {
| ------- ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail expressionDiese Fehlermeldung ist typisch Rust: klare Stelle (src/main.rs:9:33), Hinweis was erwartet wurde (i32), was gefunden wurde (() — das Unit-Type, Rusts Äquivalent zu void), und die Erklärung warum (Block-Body hat keine tail expression). Das systematische Lesen dieser Meldungen üben wir im Artikel Compiler und Fehlerlesen.
FAQ
Warum hat println! ein Ausrufezeichen?
Weil es ein Makro ist, keine Funktion. Das ! ist Rusts Sichtbarkeitskonvention für Makro-Aufrufe. Der Grund: println! prüft den Format-String zur Compile-Zeit. println!("{} {}", x) mit zu wenigen Argumenten ist ein Compile-Fehler — das ginge mit einer normalen Funktion in einer statisch typisierten Sprache nicht so elegant.
Muss meine main-Funktion zwingend so heißen?
Ja, bei Binary-Crates. rustc sucht beim Bauen eines Binaries nach genau einer Funktion namens main. Library-Crates (src/lib.rs) haben keine main-Funktion. Die Signatur darf aber variieren: fn main(), fn main() -> Result<(), Box<dyn std::error::Error>> oder seit kürzerem auch fn main() -> ExitCode sind alle gültig.
Wo landet mein Binary?
Bei Cargo: target/debug/<projektname> für Debug-Builds, target/release/<projektname> für Release-Builds. Auf Windows mit .exe. Bei direktem rustc hello.rs landet die Binary neben der Quelldatei. Das target/-Verzeichnis kannst du jederzeit löschen — Cargo baut es beim nächsten Befehl neu.
Warum ist mein erster Build so groß (mehrere MB)?
Weil Rust die Standard-Bibliothek standardmäßig statisch einbindet. Auf Linux sind 3–4 MB normal für ein Hello-World im Debug-Build. Im Release-Build mit strip = true in Cargo.toml und panic = "abort" schrumpft das auf 200–400 KB. Für embedded oder WASM gibt es noch aggressivere Optionen — Thema im Tooling-Kapitel.
Was bedeutet das edition = "2024" in Cargo.toml?
Die Sprach-Edition, gegen die der Compiler dein Crate parst. 2024 ist die neueste Edition zum Zeitpunkt dieses Artikels. Editionen sind opt-in inkompatible Sprach-Updates — siehe den eigenen Artikel zu Editionen. Wenn du nicht weißt was du wählen sollst: die neueste, also 2024.
Warum gibt es kein return in fn addiere?
Du kannst return a + b; schreiben, aber idiomatisches Rust nutzt die letzte Expression ohne Semikolon als Rückgabewert. Das ist Teil von Rusts „expression-oriented" Design — fast alles ist eine Expression. Ein return brauchst du nur, wenn du frühzeitig aus einer Funktion zurückkehren willst.
Muss ich vor jedem Build cargo clean machen?
Nein, fast nie. Cargo macht inkrementelle Builds — nur was sich geändert hat, wird neu kompiliert. cargo clean löscht den gesamten target/-Ordner und wird nur gebraucht, wenn der Cache wirklich kaputt scheint (sehr selten) oder du Plattenplatz zurückgewinnen willst.
Kann ich Rust-Code interaktiv ausprobieren wie in Python?
Nicht in der Stdlib. Es gibt play.rust-lang.org als Online-REPL-Ersatz, und das Crate evcxr_repl bietet eine echte Jupyter-Integration. Für schnelle Experimente reicht aber auch cargo new sandbox && cargo run — Cargo ist schnell genug, dass es sich nicht wie eine Hürde anfühlt.
Weiterführende Ressourcen
Externe Quellen
- The Rust Book – Hello, World!
- The Rust Book – Hello, Cargo!
- The Cargo Book – Build Profiles
- rustc Documentation – How rustc works
- std::macro.println – Standard-Library-Doku