Ferris Talk #10: Constant Fun mit Rust – const fn

Seite 2: Code zur Übersetzungszeit ausführen

Inhaltsverzeichnis

Neben den üblichen, konstanten Werten erlaubt Rust mit const auch das Ausführen von Code beim Kompilieren. Das folgende Codebeispiel dient als Einstieg. Das Sample 6 aus dem Rust Playground ist in mehrerlei Hinsicht interessant.

Zunächst handelt es sich um eine anonyme Konstante, die also keinen Namen hat. Manch einer mag sich jetzt fragen, ob eine Konstante sinnvoll ist, die sich ohne Namen nirgends referenzieren lässt. Bevor das Sinn ergibt, wird es noch eigenartiger: Die Konstante ist vom Typ Unit, hat also nur einen möglichen Wert: ().

Sinnvoll wird das, indem ein Codeblock den Unit-Wert erzeugt. Durch const findet das Ausführen des Codes jedoch nicht zur Laufzeit statt, sondern Rust führt ihn zur Übersetzungszeit aus. Das folgende Listing enthält Code, der prüft, ob die Struktur IHaveNumbers den Trait HasNumbers implementiert.

Das Konzept, Code auszuführen, um den Wert einer Konstante zu ermitteln, heißt in Rust Constant Evaluation. Für Codeblöcke, die sich konstant evaluieren lassen, gibt es Einschränkungen, und nicht jeder Rust-Code ist in diesem Kontext erlaubt. Auf einige dieser Einschränkungen gehen spätere Beispiele noch ein.

trait HasNumbers {
    const NUMBERS: [i32; 5];
    const LAST_NUMBER: i32 = 5;
}
struct IHaveNumbers {}
impl HasNumbers for IHaveNumbers {
    const NUMBERS: [i32; 5] = [1, 2, 3, 4, IHaveNumbers::LAST_NUMBER];
}

// Constants do not need to have a name. They can be unnamed.
// This is useful for running code at compile time.
const _: () = {
    // Code that makes sure that IHaveNumbers implements HasNumbers
    use std::marker::PhantomData;
    struct ImplementsMyTrait<T: HasNumbers>(PhantomData<T>);
    let _ = ImplementsMyTrait(PhantomData::<IHaveNumbers>);
};

fn main() {
}

Konstant evaluierte Codeblöcke können natürlich nicht nur Unit zurückgeben. Sie können jeden beliebigen Wert zur Übersetzungszeit berechnen, um ihn einer Konstanten oder einer statischen Variablen zuzuweisen. Es lohnt sich, diese Möglichkeit anhand eines weiteren Codebeispiels genauer anzusehen (beispielsweise im Rust Playground, Sample 7).

Die Funktion five_numbers ist eine const fn. Sie ermittelt die Zahlen 1 bis 5 mithilfe einer Schleife. Im Beispiel sollte das Augenmerk auf den Kommentaren im Code liegen, die auf Einschränkungen bei konstanten Funktionen in Rust hinweisen.

Der Compiler ermittelt die Werte beim Übersetzen. Das lässt sich überprüfen, indem man sich im Rust-Playground den generierten Assembler-Code ansieht (siehe weiterhin Sample 7 im Rust Playground). Dort steht unter anderem das generierte Array mit fünf Integer-Werten bereit.

In der main-Methode sieht man die Aufrufe der five_numbers-Funktion. Einmal findet der Aufruf zur Übersetzungszeit statt (zum Ermitteln des Wertes einer Konstante) und einmal zur Laufzeit (zum Bestimmen des Wertes einer Variablen).

Die Methode numbers ist eine Verallgemeinerung der zuvor erwähnten konstanten Funktion. Sie kann eine beliebige Anzahl an Werten erzeugen. Die gewünschte Anzahl wird in dieser Umsetzung als const generic-Parameter an die Methode übergeben.

// Functions can be constant to be usable
// in const and static scenarios.
// Note that some limitations apply (see examples below).
const fn five_numbers() -> [i32; 5] {
    let mut numbers = [0i32; 5];

    // Note that for loops can not (yet) be used
    // in constant functions.
    // So we have to go for a while loop.
    let mut i = 0;
    while i < 5 {
        numbers[i] = i as i32 + 1;
        i += 1;
    }

    numbers
}

// Const functions can be generic and can also receive const
// generic parameters.
const fn numbers<const N: usize>() -> [i32; N] {
    let mut numbers = [0i32; N];

    let mut i = 0;
    while i < N {
        numbers[i] = i as i32 + 1;
        i += 1;
    }

    numbers
}

fn main() {
    const FIVE_NUMBERS: [i32; 5] = five_numbers();
    println!("{:?}", FIVE_NUMBERS);

    const TEN_NUMBERS: [i32; 10] = numbers();
    println!("{:?}", TEN_NUMBERS);

    // Const functions can also be called at runtime.
    // They are not limited to constants or static variables.
    let five_numbers = five_numbers();
    println!("{:?}", five_numbers);
}

Die zuvor gezeigten Beispiele haben die Grundlagen der konstanten Evaluierung in Rust veranschaulicht. Im folgenden Codebeispiel geht es an die Umsetzung eines etwas größeren, praxisnahen Anwendungsfalls (siehe Rust Playground, Sample 8). Mehrere konstante Strings sind zur Übersetzungszeit zu verketten. Rust verfügt zwar über ein eingebautes concat!-Makro, allerdings akzeptiert es nur Literale. Diese Einschränkung ist in der Umsetzung unerwünscht. Der Beispielcode ist in folgende Teile gegliedert:

  • len kann zur Übersetzungszeit die Gesamtlänge aller Strings in einem Slice ermitteln.
  • concat allokiert beim Kompilieren einen ausreichend großen Puffer und kopiert einen String nach dem anderen hinein.
  • Das Makro my_concat! vereinfacht den Einsatz von len und concat.
  • Die main-Methode zeigt, wie sich die konstanten Funktionen und das Makro aufrufen lassen.
// Slightly more complex samples for concatenation
// of strings in const fn.
// Inspired by crates like
// https://docs.rs/const-str/latest/const_str/.

/// Gets total length of all provided strings
const fn len(strs: &[&str]) -> usize {
    let mut result = 0;
    let mut remaining = strs;

    // Note that we cannot use iter() here because
    // it is not a constant expression. Therefore, we
    // have to iterate using slice deconstruction.
    while let [current, tail @ ..] = remaining {
        result += current.len();
        remaining = tail;
    }

    result
}

/// Helper struct for concatenation of strings in const fn.
struct Buf<const N: usize>([u8; N]);

/// Concatenates all provided strings into a single string *at compile time*.
const fn concat<const N: usize>(strs: &[&str]) -> Buf<N> {
    let mut buffer = [0; N];
    let mut position_in_buffer = 0;

    let mut remaining = strs;
    while let [current, tail @ ..] = remaining {
        let x = current.as_bytes();
        let mut i = 0;

        // Note that we cannot use
        // copy_from_slice because mutable
        // references are not allowed in const functions.
        // buffer.copy_from_slice(x);

        // Note that for loop is not (yet) allowed
        // in const functions.
        // We have to use while instead.
        while i < x.len() {
            buffer[position_in_buffer] = x[i];
            position_in_buffer += 1;
            i += 1;
        }

        remaining = tail;
    }

    Buf(buffer)
}

macro_rules! my_concat {
    ($($x: expr),+ $(,)?) => {{
        const STRS: &[&str] = &[$($x),+];
        const LEN: usize = len(STRS);
        const CONCAT_BUF: Buf<LEN> = concat(STRS);
        unsafe { core::str::from_utf8_unchecked(&CONCAT_BUF.0) }
    }}
}

fn main() {
    // Define constant strings and concat them at compile time
    const STRS: &[&str] = &["Hello", " ", "World!"];
    const LEN: usize = len(STRS);
    const CONCAT_BUF: Buf<LEN> = concat(STRS);
    const GREETING: &str = unsafe { core::str::from_utf8_unchecked(&CONCAT_BUF.0) };
    println!("{}", GREETING);

    // Const functions can be called at runtime, too
    let strs: &[&str] = &["Hello", " ", "World!"];
    let concat_buf: Buf<LEN> = concat(strs);
    let greeting: &str = unsafe { core::str::from_utf8_unchecked(&concat_buf.0) };
    println!("{}", greeting);


    const CAT: &str = "CAT";
    const MOUSE: &str = "MOUSE";

    // Rust's built-in concat! macro only accepts literals.
    // We cannot concat two string constants.
    //const GREETING3: &str = concat!(CAT, " LOVES ", MOUSE);

    // Our macro makes it easy to call the concat const fns
    // we defined before. It is not limited to literals.
    const GREETING2: &str = my_concat!(CAT, " LOVES ", MOUSE);

    println!("{}", GREETING2);
}

Das im Mai veröffentlichte Rust 1.61 bringt in Sachen const fn wichtige Neuerungen. Einige davon werden wir im Folgenden an Beispielen zeigen. Eine komplette Auflistung ist in der englischsprachigen Ankündigung der Version 1.61 im Rust-Blog zu finden.

Das erste Beispiel zum Thema Neuerungen zeigt, dass Rust jetzt bei konstanten Funktionen Trait Bounds beherrscht. Im Code verlangt die Funktion nth die Übergabe eines Array, dessen Elemente den Copy-Trait implementieren (siehe Rust Playground, Sample 9).

#![allow(dead_code)]

// Constants in Rust do not need to be basic data types.
// Structs can also be constant.
#[derive(Debug, Clone, Copy)]
struct Customer<'a> {
    name: &'a str,
    age: i32,
}
const CUSTOMER: Customer = Customer {
    name: "John",
    age: 42,
};

// Brand new (1.61, stable): Trait bounds on generic parameters for
// const functions are now supported.
const fn nth<T: Copy, const N: usize>(items: [T; N], index: usize) -> T {
    items[index]
}

fn main() {
    const CUSTOMERS: [Customer; 2] = [
        Customer {
            name: "John",
            age: 30,
        },
        Customer {
            name: "Jane",
            age: 25,
        },
    ];
    const NTH_CUSTOMER: Customer = nth(CUSTOMERS, 1);
    println!("{:?}", NTH_CUSTOMER);
}

Das zweite Beispiel zeigt, dass konstante Funktionen in Rust neuerdings impl Traits und dyn Traits unterstützen (auch zu finden im Rust Playground, Sample 10).

trait Animal {
    fn make_sound<'a>(&self) -> &'a str;
}
struct Cat {}
struct Dog {}
impl Animal for Cat {
    fn make_sound<'a>(&self) -> &'a str {
        "meow"
    }
}
impl Animal for Dog {
    fn make_sound<'a>(&self) -> &'a str {
        "woof"
    }
}

// Brand new (1.61): Arguments and return values for const fns
// now support impl trait.
const fn favorite_animal() -> impl Animal {
    Cat {}
}

// Brand new (1.61): Arguments and return values for const fns
// now support dyn trait.
const fn animal_by_sound<'a>(can_purr: bool) -> &'a dyn Animal {
    match can_purr {
        true => &Cat {},
        false => &Dog {},
    }
}

const fn _can_purr(_animal: &dyn Animal) -> Result<bool, &str> {
    // Note that the following if statement
    // is not possible in const fn
    // because calls to trait methods are not allowed.
    // if (animal.make_sound() == "meow") {
    //     return Ok(true);
    // }

    Err("Sorry, cannot find that out")
}

fn main() {
    const FAVORITE_ANIMAL: &dyn Animal = &favorite_animal();
    println!("My favorite animal makes {}", FAVORITE_ANIMAL.make_sound());

    const PURRING_ANIMAL: &dyn Animal = animal_by_sound(true);
    println!("Animals that can purr say {}", PURRING_ANIMAL.make_sound());
}

Die Arbeit, die das Core-Team von Rust und insgesamt über 300 Beitragende unter anderem von Unis und großen Tech-Konzernen in das Entwickeln neuer Funktionsweisen konstanter Funktionen in Rust investieren, ermöglicht es, in der Standardbibliothek mehr auf const fn umzustellen. Das letzte Beispiel dieser Ausgabe der Kolumne zeigt das am Beispiel des Typs std::sync::Mutex (siehe Rust Playground, Sample 11).

Wer in der stabilen Rust-Version 1.61 einen Mutex statisch deklarieren möchte, braucht dafür Tricks (Static Mutex – mehr dazu auf Stack Overflow in der Diskussion zur Frage: "How do I create a global, mutable Singleton?"). Die Nightly-Version von Rust ändert das. Mutex::new wurde zur const fn und man kann daher Zusätze wie lazy_static zur Initialisierung einer statischen Variable verwenden.

// Brand new (1.63): Mutex::new and RwLock::new are const functions.
// No need for lazy_static & friends anymore (see also
// https://stackoverflow.com/a/27826181/3548127).
static ARRAY: std::sync::Mutex<Vec<u8>> = std::sync::Mutex::new(vec![]);

fn main() {
    {
        let mut arr = ARRAY.lock().unwrap();
        for _ in 0..10 {
            arr.push(1);
        }
    }
    println!("Called push {} times", ARRAY.lock().unwrap().len());
}