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:

Rust hello.rs
fn main() {
    println!("Hallo, Welt!");
}

Im selben Verzeichnis im Terminal:

Rust rustc direkt
rustc hello.rs
./hello          # auf Windows: .\hello.exe

Ausgabe:

Rust
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:

  1. Parsinghello.rs wird zu einem abstrakten Syntaxbaum (AST).
  2. Name Resolution — alle Bezeichner werden ihren Definitionen zugeordnet.
  3. Type Checking — der Typ-Checker verifiziert, dass println! mit den übergebenen Argumenten kompatibel ist.
  4. Borrow Checking — der Borrow Checker prüft Ownership-, Reference- und Lifetime-Regeln. (In hello.rs gibt's da wenig zu prüfen.)
  5. MIR-Generierung — Mid-level Intermediate Representation, Rusts eigene Zwischensprache.
  6. LLVM-Codegen — MIR wird zu LLVM IR übersetzt; LLVM optimiert und erzeugt Maschinencode.
  7. 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.

Rust Neues Projekt mit Cargo
cargo new hello
cd hello
cargo run

Ausgabe:

Rust
   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ächsten cargo run wird der Build incrementell wiederverwendet.
  • Binary ausgeführt ohne dass du den Pfad kennen musst.

Die generierten Dateien

Rust hello/
hello/
├── Cargo.toml      ← Projekt-Metadaten und Dependencies
├── Cargo.lock      ← Exakte Versionen aller Crates
├── .gitignore
└── src/
    └── main.rs     ← Einstiegspunkt

Cargo.toml enthält am Anfang nur das Nötigste:

Rust Cargo.toml
[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:

Rust src/main.rs (annotiert)
fn main() {                       // Einstiegspunkt — muss main heißen
    println!("Hallo, Welt!");     // Makro-Aufruf (siehe !)
}

Drei Dinge sind hier konzeptuell wichtig:

  • fn ist das Schlüsselwort für eine Funktion. Klein geschrieben, kurz — Rust-Syntax bevorzugt Knappheit.
  • main ist als Funktionsname konventionell, aber gleichzeitig zwingend: Wenn dein Crate ein Binary ist (also src/main.rs hat), erwartet rustc eine Funktion main mit dieser Signatur. Sie kann auch fn 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. Bei println! 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):

Rust Expansion (gedanklich)
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.

Rust Beide Profile
cargo build            # Debug-Build  → target/debug/hello
cargo build --release  # Release-Build → target/release/hello
cargo run --release    # Release direkt ausführen

Der 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.

Aspektcargo buildcargo build --release
Optimierungslevelopt-level = 0opt-level = 3
Debug-Infodebug = truedebug = false (außer du änderst es)
Build-Zeitschnelllangsam
Laufzeitlangsamoptimiert, oft 10×+ schneller
Output-Pfadtarget/debug/target/release/
Wann nutzenEntwickeln, TestsBenchmarken, 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 in target/debug/ (oder target/release/ mit --release).
  • cargo runcargo 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 check in einem Watch-Loop (cargo watch -x check aus dem Crate cargo-watch).
  • Bei Tests: cargo test.
  • Vor Commit: cargo clippy und cargo 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:

Rust src/main.rs
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
}
Output
Hallo, Welt!
3 + 4 = 7

Hier 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:

Rust
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 expression

Diese 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

/ Weiter

Zurück zu Grundlagen

Zur Übersicht