zurück zum Artikel

Ferris Talk #15: Bedingte Kompilierung in Rust

Rainer Stropek

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

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 [16]. 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 [17] 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 [18] enthält eine vollständige Liste aller vordefinierten Konfigurationseinstellungen.

Im Beispiel hat der Rust-Compiler rustc den Code übersetzt. In der Praxis werden jedoch die meisten den Paketmanager Cargo verwenden, statt rustc direkt aufzurufen. In Verbindung mit Cargo ist es üblich, statt der zuvor gezeigten, einfachen Konfigurationseinstellungen [features] zu verwenden, die in der Datei Cargo.toml definiert sind. Sie lassen sich einerseits mit bedingter Kompilierung nutzen, um Codeabschnitte zu aktivieren oder zu deaktivieren. Andererseits können sie Abhängigkeiten steuern. Häufig sind einige Dependencies zu Crates nur dann erforderlich, wenn ein gewisses Feature aktiv ist.

Folgendes Beispiel demonstriert die bedingte Kompilierung. Cargo.toml modelliert die drei Varianten unseres Salates als Features. Außerdem legt sie fest, dass standardmäßig (default) keine Variante aktiv ist. Zusätzlich enthält sie das feature all, um zu demonstrieren, wie sich mehrere Features unter einem Namen kombinieren lassen.

[package]
name = "salad_maker_with_features"
version = "0.1.0"
edition = "2021"

[features]
default = []
traditional = []
vegetarian = []
vegan = []
all = ["traditional", "vegetarian", "vegan"]

[dependencies]

Der folgende Rust-Code zeigt die bedingte Kompilierung auf Basis von Features. Um das Beispiel übersichtlich zu halten, fehlen Codeteile, die nicht zusätzlich zum Verständnis beitragen. Der vollständige Code findet sich auf GitHub [19].

fn main() {
  // Note that we are now checking cargo features
  #[cfg(not(any(feature = "traditional", 
                feature = "vegetarian", 
                feature = "vegan")))]
  {
    println!(r"You haven't specified whether the
recipe should be traditional, vegetarian, or vegan.");
    panic!();
  }

  #[cfg(any(all(feature = "traditional", 
                feature = "vegetarian"), 
            all(feature = "traditional", 
                feature = "vegan"), 
            all(feature = "vegetarian", 
                feature = "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:");
    
    [...]

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

  #[cfg(feature = "traditional")]
  {
      println!("- Anchovies");
      println!("- Egg yolks");
  }

  [...]
}

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.");
    
  match (cfg!(feature = "traditional"), 
         cfg!(feature = "vegetarian"), 
         cfg!(feature = "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!"),
  };

  [...]
}

Beim Bearbeiten des Codes verwenden der Rust Analyzer und damit auch Visual Studio Code das Feature default, das im Szenario bewusst fehlt. Die automatische Auswahl erschwert die Entwicklungsarbeit, da der Editor Code, der korrekt und aktiv ist, als inaktiv markiert. Der Grund ist die fehlende Featureauswahl, die zwar vor dem Kompilieren erfolgt, beim Editieren aber noch fehlt. Die folgende Abbildung zeigt, wie der Beispielcode in Visual Studio Code aussieht:

Ohne Featureauswahl zeigt Visual Studio Code den Code im [code]cfg[/code]-Block als inaktiv dar (Abb. 1).

Zum Glück lässt sich dieses Verhalten korrigieren: Die Einstellung rust-analyzer.cargo.features teilt Visual Studio Code mit, welches Feature man für den Rust Analyzer verwenden möchte. Die folgende Abbildung zeigt, wie sich die Visualisierung des aktiven Codes im Editor verändert, wenn man das Feature vegan aktiviert:

Bei der Auswahl eines Features hebt Visual Studio Code den zugehörigen Block als aktiv hervor (Abb. 2).

Häufig kommt bedingte Kompilierung in Zusammenhang mit automatisierten Tests zum Einsatz. Das Attribut #[cfg(test)] dient dazu, den Code für Tests nur beim Aufruf von cargo test zu kompilieren. Der folgende Beispielcode bleibt beim Thema Caesar Salad, aber diesmal erfolgt die Auswahl der Rezeptvariante nicht über [features], sondern durch die Übergabe an die Methode get_ingredients zur Laufzeit.

Der automatisierte Test no_anchovies_in_vegetarian soll sicherstellen, dass in der vegetarischen Variante keine Sardellen enthalten sind. Diesen Code übersetzt der Compiler nur beim Aufruf von cargo test, nicht jedoch bei cargo build.

#[derive(Debug, PartialEq, Eq)]
pub enum RecipeType { Traditional, Vegetarian, Vegan, }

pub mod caesar_salad {
  use super::*;

  // Get ingredients for a Caesar salad
  pub fn get_ingredients(recipe_type: RecipeType) 
    -> Vec<&'static str> 
  {
    let mut ingredients = vec![
      "Romaine lettuce", "Croutons", "Lemon juice",
      "Olive oil", "Salt", "Black pepper",
    ];

    ingredients.push(match recipe_type {
      RecipeType::Traditional | 
      RecipeType::Vegetarian => "Parmesan cheese",
      _ => "Vegan Parmesan cheese substitute",
    });

    ingredients.extend_from_slice(match recipe_type {
      RecipeType::Traditional 
        => &["Anchovies", "Egg yolks"],
      RecipeType::Vegetarian 
        => &["Capers", "Egg yolks"],
      _ => &["Capers", "Vegan mayonnaise"],
    });

    ingredients
  }
}

// The code for automated tests must only be
// compiled when the test suite is run (cargo test).
#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn no_anchovies_in_vegetarian() {
    let ingredients = 
      caesar_salad::get_ingredients(RecipeType::Vegetarian);
    // Make sure there are no anchovies in a 
    //vegetarian salad
    assert!(!ingredients.contains(&"Anchovies"));
  }

  // Add additional tests here
}

Neben cfg existiert ein weiteres Attribut namens cfg_attr. Während cfg dazu dient, Code bedingt zu kompilieren, erlaubt cfg_attr das bedingte Hinzufügen von Attributen. Diese Option erscheint auf den ersten Blick ungewöhnlich und Rust-Einsteigerinnen und -Einsteiger erkennen vielleicht nicht sofort, wie wichtig diese Sprachfunktion ist. Ein weiteres Beispiel bringt Licht ins Dunkel. Es führt alle besprochenen Aspekte von bedingter Kompilierung zusammen. Die Anforderungen für den Code sehen folgendermaßen aus:

Die Umsetzung beginnt mit dem Erstellen der Cargo.toml-Datei. Neu bei den definierten Features ist, dass sich Abhängigkeiten zu externen Crates bedingt hinzufügen lassen. Das ist notwendig, da die Crates serde_json und serde_yaml nur dann erforderlich sind, wenn die Features json beziehungsweise yaml aktiv sind.

toml
[package]
name = "recipe_reader"
version = "0.1.0"
edition = "2021"

[features]
default = ["json"]
all = ["json", "yaml"]
json = ["dep:serde_json"]
yaml = ["dep:serde_yaml"]

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", optional = true }
serde_yaml = { version = "0", optional = true }
anyhow = "1"

[dev-dependencies]
mockall = "0"

Im folgenden Code liegt der Schwerpunkt auf dem cfg_attr-Attribut. Es wird verwendet, um über das mockall-Crate im Rahmen von Tests ein Mock-Objekt für einen Trait zu erstellen. Dadurch lassen sich die zwei Funktionen des Codes entkoppeln: Lesen der Datei auf der einen und Umwandeln des Rezepts in Markdown auf der anderen Seite.

Die Kommentare im Code weisen auf die wichtigen Aspekte beim Verwenden von cfg und cfg_attr hin.

#[cfg(any(feature = "json", feature = "yaml"))]
use std::{fs::File, io::Read};

use anyhow::Result;
use serde::Deserialize;

// We only reference automock when testing
#[cfg(test)]
use mockall::automock;

#[derive(Debug, Deserialize)]
pub struct Recipe {
  pub title: String,
  pub ingredients: Option<Vec<String>>,
  pub steps: Option<Vec<String>>,
}

// We add the automock attribute to the trait
// ONLY when testing. This is where cfg_attr
// comes in handy.
#[cfg_attr(test, automock)]
pub trait RecipeReader {
  fn read_from_file(&self, path: &str) -> Result<Recipe>;
}

// The json module with the JsonRecipeReader is
// only compiled when the json feature is enabled.
#[cfg(feature = "json")]
pub mod json
{
  use super::*;

  pub struct JsonRecipeReader();

  impl RecipeReader for JsonRecipeReader {
     fn read_from_file(&self, path: &str) 
       -> Result<Recipe> {
       let mut contents = String::new();
       File::open(path)?.read_to_string(&mut contents)?;
       Ok(serde_json::from_str(&contents)?)
     }
  }
}

// The yaml module with the YamlRecipeReader is
// only compiled when the yaml feature is enabled.
#[cfg(feature = "yaml")]
pub mod yaml
{
  use super::*;
    
  pub struct YamlRecipeReader();

  impl RecipeReader for YamlRecipeReader {
    fn read_from_file(&self, path: &str) 
      -> Result<Recipe> 
    {
      let mut contents = String::new();
      File::open(path)?.read_to_string(&mut contents)?;
      Ok(serde_yaml::from_str(&contents)?)
    }
  }
}

pub struct RecipeMarkdownFactory();

impl RecipeMarkdownFactory {
  pub fn file_to_markdown(&self, 
                          path: &str, 
                          reader: impl RecipeReader) 
    -> Result<String> 
  {
    // Read recipe from file using a reader
    let recipe = reader.read_from_file(path)?;

    // Convert recipe into markdown
    let mut markdown = String::new();
    markdown.push_str(&format!("# {}\n\n", 
                      recipe.title));
    if let Some(i) = recipe.ingredients {
      markdown.push_str("## Ingredients\n\n");
      for ingredient in i {
        markdown.push_str(&format!("* {ingredient}\n"));
      }
      markdown.push('\n');
    }

    if let Some(s) = recipe.steps{
      markdown.push_str("## Steps\n\n");
      for (i, step) in s.iter().enumerate() {
        markdown.push_str(&format!("{}. {step}\n",
                          i + 1));
      }
    }

    Ok(markdown)
  }
}

// The code for automated tests must only be 
// compiled when the test suite is run (cargo test).
#[cfg(test)]
mod tests {
  use mockall::predicate::eq;

  use super::*;

  // Parts of the tests are only compiled when the 
  // corresponding feature is enabled.
  #[cfg(feature = "json")]
  use super::json::*;
  #[cfg(feature = "yaml")]
  use super::yaml::*;

  #[cfg(any(feature = "json", feature = "yaml"))]
  fn get_expected_markdown() -> String {
    let mut markdown = String::new();
    File::open("caesar_expected.md")
      .unwrap().read_to_string(&mut markdown).unwrap();
    markdown
  }

  #[cfg(feature = "json")]
  #[test]
  fn json_caesar_salad_to_markdown() {
    let reader = JsonRecipeReader();
    let markdown_factory = RecipeMarkdownFactory();
    let markdown = 
    markdown_factory
     .file_to_markdown("caesar_salad.json", 
                       reader).unwrap();

    assert_eq!(markdown, get_expected_markdown());
  }

  #[cfg(feature = "yaml")]
  #[test]
  fn yaml_caesar_salad_to_markdown() {
    let reader = YamlRecipeReader();
    let markdown_factory = RecipeMarkdownFactory();
    let markdown = 
      markdown_factory
        .file_to_markdown("caesar_salad.yaml", 
                          reader).unwrap();

    assert_eq!(markdown, get_expected_markdown());
  }

  #[test]
  fn no_ingredients() {
    // Here we use the mockall crate to create a mock
    // of the RecipeReader trait. We can then use this
    // mock to test the RecipeMarkdownFactory without
    // having to read from a file.
    let mut reader_mock = MockRecipeReader::new();
    reader_mock
      .expect_read_from_file()
      .with(eq("no_ingredients.json"))
      .returning(|_| {
        Ok(Recipe {
          title: "No Ingredients".to_string(),
          ingredients: None,
          steps: Some(vec!["Step 1".to_string(), 
                           "Step 2".to_string()]),
        })
    });
        
    let markdown_factory = RecipeMarkdownFactory();
    let markdown = markdown_factory
     .file_to_markdown("no_ingredients.json", 
                       reader_mock).unwrap();

    assert!(!markdown.contains("## Ingredients"));
  }
}

Rust 1.72 hat eine kleine, aber feine Neuerung für die bedingte Kompilierung von Features mitgebracht. Folgender Code nutzt das oben gezeigte Crate, um eine Rezeptdatei im YAML-Format zu lesen und sie in Markdown umzuwandeln:

use recipe_reader::{yaml, RecipeMarkdownFactory};

fn main() {
  let reader = yaml::YamlRecipeReader();
  let factory = RecipeMarkdownFactory();
  let markdown = 
    factory.file_to_markdown("caesar_salad.yaml", 
                             reader).unwrap();
  println!("{markdown}");
}

Das Crate hat jedoch das yaml-Feature nicht automatisch aktiviert, sondern standardmäßig ist nur das json-Feature eingeschaltet. Wer vor Rust 1.72 die Aktivierung von yaml vergessen hatte, erhielt eine Fehlermeldung, die lediglich anmahnte, dass recipe_reader::yaml nicht existiert. Das liegt daran, dass der Compiler das zugehörige Modul wegen des cfg-Attributs komplett ignoriert. Er "vergisst" somit, dass es das Modul überhaupt gibt. Daher konnte er keinen Hinweis darauf geben, dass sich der Fehler durch das Aktivieren eines Features beheben lässt.

Seit Version 1.72 merkt sich der Compiler, dass der Code-Teil existiert, er ihn aber beim bedingten Kompilieren ausgeblendet hat. Wenn er auf einen Fehler stößt, kann er darauf hinweisen, dass der referenzierte Code-Teil hinter einem cfg-Attribut versteckt ist. Folgende Fehlermeldung mit dem Hinweis auf das auskonfigurierte Feature spuckt der Compiler seitdem aus:

error[E0432]: unresolved import [code]recipe_reader::yaml[/code]
  --> src/main.rs:1:21
   |
1  | use recipe_reader::{yaml, RecipeMarkdownFactory};
   |                     ^^^^ no `yaml` in the root
   |
note: found an item that was configured out             <<< NEW IN 1.72
  --> /home/rainer/github/rust-samples/cfg/04_recipe_reader/src/lib.rs:39:9
   |
39 | pub mod yaml                                       <<< NEW IN 1.72
   |         ^^^^                                       <<< NEW IN 1.72
   = note: the item is gated behind the `yaml` feature  <<< NEW IN 1.72

Rust bietet zahlreiche Methoden für das bedingte Kompilieren. Speziell durch [features] für Cargo hebt sich die Sprache von vielen anderen ab. Wer eine Bibliothek nutzt, ist nicht gezwungen, ihre Funktionen komplett einzubinden. Einzelne Features zu deaktivieren, verkürzt die Übersetzungszeit und vermeidet unnötige Abhängigkeiten, indem der Compiler nur die benötigten Funktionen übernimmt.

Version 1.72 bringt als Sahnehäubchen eine Ergänzung für die bedingte Kompilierung, die in der Praxis hilft, Fehler durch vergessene Features zu vermeiden.

Ferris Talk - Neuigkeiten zu Rust

Rainer Stropek

ist Softwareentwickler, Trainer, Autor und Vortragender im Microsoft-Umfeld und seit über 25 Jahren als Unternehmer in der IT-Industrie tätig.

Er gründete und führte in dieser Zeit mehrere IT-Dienstleistungsunternehmen. Neben der Tätigkeit als Trainer und Berater in seiner Firma software architects [20] entwickelt er mit seinem Team die preisgekrönte Software time cockpit [21]. Rainer hat Abschlüsse der höheren technischen Schule für Informatik Leonding (AT) sowie der University of Derby (UK). Er ist Autor mehrerer Fachbücher und Artikel in Magazinen im Umfeld von Microsoft .NET und C#, Azure, Go und Rust. Seine technischen Schwerpunkte sind Cloud Computing, die Entwicklung verteilter Systeme sowie Datenbanksysteme.

Regelmäßig tritt er als Speaker und Trainer auf namhaften Konferenzen in Europa und den USA auf. 2010 wurde er von Microsoft zu einem der ersten MVPs (Most Valuable Professionals) für die Azure-Plattform ernannt. Seit 2015 ist er Microsoft Regional Director. 2016 hat er zudem den MVP Award für Visual Studio und Developer Technologies erhalten.

(rme [22])


URL dieses Artikels:
https://www.heise.de/-9337115

Links in diesem Artikel:
[1] https://www.heise.de/hintergrund/Ferris-Talk-1-Iteratoren-in-Rust-6175409.html
[2] https://www.heise.de/hintergrund/Ferris-Talk-2-Abstraktionen-ohne-Mehraufwand-Traits-in-Rust-6185053.html
[3] https://www.heise.de/hintergrund/Ferris-Talk-3-Neue-Rust-Edition-2021-ist-da-mit-Disjoint-Capture-in-Closures-6222248.html
[4] https://www.heise.de/hintergrund/Ferris-Talk-4-Asynchrone-Programmierung-in-Rust-6299096.html
[5] https://www.heise.de/hintergrund/Ferris-Talk-5-Tokio-als-asynchrone-Laufzeitumgebung-ist-ein-Fast-Alleskoenner-6341018.html
[6] https://www.heise.de/hintergrund/Ferris-Talk-6-Ein-neuer-Trick-fuer-die-Formatstrings-in-Rust-6505377.html
[7] https://www.heise.de/hintergrund/Ferris-Talk-7-Vom-Ungetuem-zur-Goldrose-eine-kleine-Rust-Refactoring-Story-6658167.html
[8] https://www.heise.de/hintergrund/Ferris-Talk-8-Wasm-loves-Rust-WebAssembly-und-Rust-jenseits-des-Browsers-7064040.html
[9] https://www.heise.de/hintergrund/Ferris-Talk-9-Vom-Builder-Pattern-und-anderen-Typestate-Abenteuern-7134143.html
[10] https://www.heise.de/hintergrund/Ferris-Talk-10-Constant-Fun-mit-Rust-const-fn-7162074.html
[11] https://www.heise.de/hintergrund/Ferris-Talk-11-Memory-Management-Speichermanagement-in-Rust-mit-Ownership-7195773.html
[12] https://www.heise.de/hintergrund/Ferris-Talk-12-Web-APIs-mit-Rust-erstellen-7321340.html
[13] https://www.heise.de/hintergrund/Ferris-Talk-13-Rust-Web-APIs-und-Mocking-mit-Axum-7457143.html
[14] https://www.heise.de/hintergrund/Ferris-Talk-14-Rust-bekommt-endlich-asynchrone-Methoden-in-Traits-8929334.html
[15] https://www.heise.de/hintergrund/Ferris-Talk-15-Bedingte-Kompilierung-in-Rust-9337115.html
[16] https://rust.bettercode.eu/
[17] https://rust.bettercode.eu/index.php#programm
[18] https://doc.rust-lang.org/reference/conditional-compilation.html#set-configuration-options
[19] https://github.com/rstropek/rust-samples/blob/master/cfg/02_basics_with_features/src/main.rs
[20] https://www.linkedin.com/company/software-architects-og/about/
[21] https://www.timecockpit.com/
[22] mailto:rme@ix.de