Ferris Talk #15: Bedingte Kompilierung in Rust

Rust bietet flexible Wege, um beim Kompilieren Codepassagen je nach Anforderung einzubeziehen oder auszulassen.

In Pocket speichern vorlesen Druckansicht 6 Kommentare lesen
Lesezeit: 8 Min.
Von
  • Rainer Stropek
Inhaltsverzeichnis

Wer Enums, Konstanten oder bedingte Kompilierung aus anderen Programmiersprachen kennt, erwartet ähnliche Funktionsweisen in Rust. Tatsächlich erleben viele Neulinge eine positive Überraschung, da Rust Tricks beherrscht, die in anderen weitverbreiteten Programmiersprachen fehlen. Dieser Artikel widmet sich der bedingten Kompilierung (Conditional Compilation), die schon bisher half, kompakte und weniger fehleranfällige Binaries zu erzeugen, und mit Rust 1.72 eine nützliche Neuerung erhalten hat.

Ferris Talks – die Kolumne für Rustaceans

Die beiden wichtigsten Bausteine für bedingte Kompilierung in Rust sind das cfg-Attribut und das cfg!-Makro. Beide müssen eine Bedingung enthalten, die beim Übersetzen true oder false ergibt. Das cfg-Attribut kann man über einen Codeabschnitt wie eine Zeile, einen Codeblock, eine Funktionsdefinition, ein Modul oder ein use-Statement setzen. Der Compiler ignoriert dann den markierten Code, wenn die zugehörige Bedingung false ergibt.

Das cfg!-Makro führt dagegen nicht dazu, dass Code beim Kompilieren ausgelassen wird. Der Compiler ersetzt ihn einfach durch ein true- oder false-Literal. Man kann cfg! daher überall einsetzen, wo ein boolescher Wert erlaubt ist.

Der folgende Code zeigt beide Vorgehensweisen. Er gibt drei verschiedene Varianten eines Rezepts für Caesar Salad aus: eine traditionelle mit Sardellen, eine vegetarische und eine vegane. Die Auswahl der Variante erfolgt über Konfigurationseinstellungen, die cfg überprüft. Die Kommentare im Code erklären die Varianten der bedingten Kompilierung genauer.

fn main() {
  // Note that you can combine configuration 
  // predicates with all, any, and not.
  #[cfg(not(any(traditional, vegetarian, vegan)))]
  {
    println!(r"You haven't specified whether the
recipe should be traditional, vegetarian, or vegan.");

    panic!();
  }

  #[cfg(any(all(traditional, vegetarian), 
        all(traditional, vegan), 
        all(vegetarian, vegan)))]
  {
    println!(r"You've specified more than one
recipe type. Please specify only one.");

    panic!();
  }

  print_ingredients();
  print_preparation();
}

fn print_ingredients() {
  println!("Ingredients for Caesar Salad:");
    
  println!("- Romaine lettuce");
  println!("- Croutons");
  println!("- Lemon juice");
  println!("- Olive oil");
  println!("- Salt");
  println!("- Black pepper");

  // We combine config predicates again
  #[cfg(any(traditional, vegetarian))]
  println!("- Parmesan cheese");
  #[cfg(any(vegan))]
  println!("- Vegan Parmesan cheese substitute");

  // Note that the cfg attribute can be applied 
  // to any item, not just a line of code. In 
  // this case, we're using it to conditionally
  // include a block of code. You could also 
  // conditionally define a module, struct, 
  // trait, function, etc.
  #[cfg(traditional)]
  {
    println!("- Anchovies");
    println!("- Egg yolks");
  }

  #[cfg(vegetarian)]
  {
    println!("- Capers");
    println!("- Egg yolks");
  }

  #[cfg(vegan)]
  {
    println!("- Capers");
    println!("- Vegan mayonnaise");
  }
}

fn print_preparation() {
  println!("\nPreparation:");

  println!(r"1. Wash and tear the romaine
lettuce into bite-size pieces.");
  println!(r"2. In a bowl, combine lemon juice,
olive oil, salt, and pepper.");
    
  // Instead of the config attribute, we can also 
  // use the cfg! macro. Note that it does NOT 
  // remove any code. The macro only evaluates 
  // to true or false at compile time.
  // The following code might not be the best 
  // option in real life, but it's a good
  // example of how to use the cfg! macro.
  match (cfg!(traditional), 
         cfg!(vegetarian), 
         cfg!(vegan)) {
    (true, false, false) => 
      println!(r"3. Add grated Parmesan cheese,
anchovies, and egg yolks to the bowl."),
    (false, true, false) => 
       println!(r"3. Add grated Parmesan cheese,
capers, and egg yolks to the bowl."),
    (false, false, true) => 
       println!(r"3. Add vegan Parmesan cheese 
substitute, capers, and vegan mayonnaise to the bowl."),
    _ => panic!(r"More than one recipe type? 
This should never happen!"),
    };

    println!(r"4. Mix well until the ingredients
are well-combined.");
    println!(r"5. Add the croutons and romaine
lettuce to the bowl.");
    println!(r"6. Toss until the lettuce is 
well-coated with the dressing.");
    println!(r"7. Serve immediately and enjoy
your Caesar Salad!");
}

Kompiliert man das Codebeispiel mit rustc main.rs und führt das erstellte Programm aus, kommt es zu einer Panic, da es zu Beginn prüft, ob mindestens eine Rezeptvariante konfiguriert ist. Beim Kompilieren mit rustc --cfg vegan main.rc läuft das Programm dagegen reibungslos und gibt die Schritte zum Zubereiten eines veganen Caesar Salad aus.

Online-Konferenz zu Rust

Am 24. Oktober findet die betterCode() Rust statt. Die von iX und dpunkt.verlag ausgerichtete Online-Konferenz richtet sich vor allem an diejenigen, die Rust nutzen möchten, um ihre C/C++-Codebasis zu migrieren oder zu ergänzen.

Das Programm der Konferenz bietet Vorträge zu folgenden Themen:

  • Grundlegende Unterschiede und Vorteile von Rust zu C/C++
  • Rust und C++: Migrieren und integrieren
  • Traumpaar Rust & WebAssembly
  • Details zur Ausdrucksstärke von Rust
  • Asynchrone Programmierung im Zusammenspiel mit anderen Sprachen
  • Praktischer Einsatz von Rust im industriellen Umfeld

Der Rust-Compiler setzt von Haus aus eine ganze Reihe von Konfigurationseinstellungen, auf die der Code zugreifen kann. Man kann dadurch beispielsweise herausfinden, für welches Betriebssystem die Kompilierung erfolgt (target_os-Einstellung) oder ob der Code mit Optimierungen kompiliert wird (debug_assertions-Einstellung). Darauf aufbauend können betriebssystemspezifische Codeteile oder Code für Debug-Prüfungen bedingt eingebaut beziehungsweise ignoriert werden.

Die Rust-Dokumentation enthält eine vollständige Liste aller vordefinierten Konfigurationseinstellungen.