Ferris Talk #14: Rust bekommt asynchrone Methoden in Traits

Seite 2: Generische Lifetime-Parameter für Futures

Inhaltsverzeichnis

Hier ist eine kleine Entschuldigung angebracht: Der Einfachheit halber blieb bisher ein wichtiges Detail unerwähnt, das allerdings für das komplette Bild asynchroner Methoden in Traits wichtig ist.

Anders als andere impl-Trait-Auflösungen muss eine Future die Lifetime-Parameter aller Eingabeparameter mitnehmen. Das heißt, aus

async fn read_to_string(buf: &mut String) 
  -> Result<usize>;

wird schließlich

fn read_to_string<'a>(buf: &'a mut String) -> 
  impl Future<Output = Result<usize>> + 'a;

Der Grund dafür erschließt sich bei einem Blick darauf, wie Futures intern abgebildet werden: Sie führen keinen Code aus, sondern übergeben nur die Möglichkeit, Code von einer asynchronen Laufzeitumgebung ausführen zu lassen.

Asynchrone Funktionen erstellen solche Futures und benötigen daher Referenzen auf alle Eingabeparameter. Diese Referenzen müssen gemäß dem Ownership-Prinzip von Rust genauso lange vorhanden sein wie die Future, da Letztere sonst mit Daten arbeitet, die potenziell nicht mehr existieren.

Um diese Verbindung sicherzustellen, zieht man den Lifetime-Parameter mit. Die Future darf nicht länger existieren als alle Referenzen, die als Eingabeparameter mitkommen. Der Rust-Compiler ermöglicht dank des Schlüsselwortes async die einfachere Schreibweise.

Die Lifetime-Parameter stellen allerdings den Trait mit asynchronen Methoden vor eine Herausforderung. Die äquivalente Schreibweise für den ChatSink-Trait sieht folgendermaßen aus:

pub trait ChatSink {
  type Item: Clone;
  type $<'m>: Future<Output = Result<(), 
    ChatCommErr>> + 'm;
  fn send_msg(&mut self, msg: Self::Item) 
    -> Self::$<'_>;
}

Der Code löst die Lifetime für die Future via generischem Lifetime-Parameter im assoziiertem Typ. Generische Parameter für assoziierte Typen, existieren allerdings erst seit Ende 2022 in Rust Stable.

Tatsächlich kippen GATs ( Generic Associated Types) den ersten von vielen Dominosteinen, die asynchrone Methoden in Traits möglich machen.

Die erste Version der asynchronen Methoden ist nun in Nightly gelandet und lässt sich mit dem zugehörigen feature-Makro als Einleitung ausprobieren:

#![feature(async_fn_in_trait)]
pub trait ChatSink {
  type Item: Clone;
  async fn send_msg(&mut self, msg: Self::Item) -> 
    Result<(), ChatCommErr>;
}

Allerdings zeigen sich potenziell einige Einschränkungen. Zum einen ist es aktuell nicht erlaubt, der generierten Future zusätzliche Typparameter wie Send zu übergeben, die beispielsweise sicherstellen, dass mehrere Traits problemlos dieselbe Future bearbeiten können. Das ist eine Anforderung, die in fortgeschrittenen Szenarien öfter auftaucht.

Zum anderen können asynchrone Methoden in Traits keine Trait-Objekte annehmen, womit der dynamische Dispatch unmöglich ist. Abhilfe schafft weiterhin das Crate async_trait, das den Vorgang per Makro vornimmt. So wird aus

#[async_trait]
pub trait ChatSink {
  type Item: Clone;
  async fn send_msg(&mut self, msg: Self::Item) ->
    Result<(), ChatCommErr>;
}

eine Version mit Trait-Objekten und Daten auf dem Heap:

pub trait ChatSink {
  type Item: Clone;
  fn send_msg(&mut self, msg: Self::Item) ->
    Pin<Box<dyn Future<Output = Result<(), 
                       ChatCommErr>>>>;
}

Mehr Informationen zu Traits und Trait-Objekten finden sich in Ferris Talk #2.

Für beide Problemstellungen existieren Design-Dokumente und Ideen, um sie zu lösen. Aktuell evaluiert das Rust-Team, ob die Limitierungen Blocker für das Stabilisieren der asynchronen Methoden sind.

Genauso wie GATs den Anstoß gegeben haben, asynchrone Methoden in Traits zu entwickeln, wird das neue Feature eine Revolution in der asynchronen Programmierung in Rust anstoßen. Aktuell gibt es einige Stolpersteine unter anderem beim Implementieren von Services für Webanwendungen über Tower. Die Umsetzung benötigt entweder viel Aufwand im Design von Futures oder das Struct BoxFuture, das mit Trait-Objekten arbeitet, um asynchrone Middleware-Schichten zu beschreiben. In Zukunft könnten sich solche Konstrukte simpler beschreiben lassen.

Des Weiteren kann das Rust-Team nun direkt an der Definition asynchroner Traits arbeiten, um unter anderem APIs für das Lesen und Schreiben von Dateien oder Netzwerkverbindungen zu definieren, die Runtimes wie Tokio implementieren können. Dadurch gelingt ein wichtiger Schritt zur Normalisierung asynchroner Laufzeitumgebungen und zu einem Weg für laufzeitunabhängige asynchrone Bibliotheken.

Die Async-Arbeitsgruppe bei Rust hat noch viele weitere Ideen. Nick Cameron, leitender Ingenieur und Rust-Experte bei Microsoft, stellte bei dem vom Autor mitveranstalteten Meetup Rust Linz im April 2022 einige dieser Entwicklungen vor.

Empfohlener redaktioneller Inhalt

Mit Ihrer Zustimmmung wird hier ein externes YouTube-Video (Google Ireland Limited) geladen.

Ich bin damit einverstanden, dass mir externe Inhalte angezeigt werden. Damit können personenbezogene Daten an Drittplattformen (Google Ireland Limited) übermittelt werden. Mehr dazu in unserer Datenschutzerklärung.

Zu guter Letzt sind asynchrone Methoden in Traits allerdings ein echtes Lehrstück in Sachen Rust RFCs und Designdokumente.

Das Zusammenspiel der einzelnen Teams und Working Groups ist von vielen Diskussionen geprägt, aber es funktioniert. Sprachinnovation geschieht, wenn die Anwendungsfälle gut durchdacht und definiert sind. Auch wenn einige Dinge etwas länger dauern, gibt es neue Annehmlichkeiten, ohne mit altem Code zu brechen.