Ferris Talk #14: Rust bekommt asynchrone Methoden in Traits

Asynchrone Programmierung ist in Rust nach wie vor eine Baustelle. Eine Neuerung hat das Zeug, sie deutlich voranzubringen.

In Pocket speichern vorlesen Druckansicht 7 Kommentare lesen
Lesezeit: 8 Min.
Von
  • Stefan Baumgartner
Inhaltsverzeichnis

Seit wenigen Monaten erlaubt die Nightly Version von Rust, asynchrone Methoden für Traits zu definieren. Vorher waren dafür zusätzliche Bibliotheken erforderlich. Die Neuerung ist ein wichtiger Meilenstein für das lang erwartete Async-Featureset von Rust. Doch welche Gründe gab es für die Wartezeit, und was muss noch passieren bis asynchrone Methoden in Traits endlich in der stabilen Version landen?

Ferris Talks – die Kolumne für Rustaceans

Auf der Suche nach dem Warum müssen wir bei den Grundprinzipien der asynchronen Programmierung in Rust anfangen. Die generelle Funktionsweise hat Ferris Talk # 4 beschrieben. Eine kurze Zusammenfassung:

Rust hat keine integrierte asynchrone Laufzeitumgebung, sondern setzt auf Bibliotheken. Die Sprache bietet aber sprachliche Konstrukte und Traits, mit denen die Libraries arbeiten können.

Zu den Traits gehört mit Future die Abstraktion eines Wertes, der irgendwann in der Zukunft ankommt. Funktionen, die eine Future zurückgeben, werden nicht sofort ausgeführt, sondern Anwendungen müssen sie entweder explizit an eine asynchrone Laufzeitumgebung senden oder innerhalb einer asynchronen Ausführung mit .await erwarten.

Future ist ein Trait, der den zu erwartenden Wert mittels assoziiertem Typ Output definiert. Auf die Weise lassen sich Rückgabewerte beschreiben.

trait Future {
  type Output;
  fn poll(self: Pin<&mut Self>, 
          cx: &mut Context<'_>) -> 
    Poll<Self::Output>;
}

Über die Methode poll kann die Runtime herausfinden, ob es schon einen Wert gibt, oder ob sie die gleiche Future später erneut überprüfen soll.

Die Schnittstelle einer asynchronen Funktion, die Daten in einen String Buffer liest und die Anzahl der gelesenen Bytes per Result zurückgibt, könnte folgendermaßen aussehen:

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

Dabei nutzt Rust die impl-Trait-Syntax, die beschreibt, dass die Rückgabe den Trait Future implementieren muss und ein Result-Enum mit einer usize im Ok-Wert als Output enthält.

Dieses Konstrukt lässt sich über syntaktischen Zucker abkürzen, indem das Schlüsselwort async vor der Funktion steht. Das Codestück von vorhin, wird damit in einer schöneren Schreibweise zur asynchronen Funktion:

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

Der Rust-Compiler übersetzt diese Syntax in die komplexere Variante, die mit Futures hantiert und dadurch kompatibel mit asynchronen Laufzeitumgebungen ist.

Wie geht man mit Traits um? Ein kleiner Disclaimer vorweg: Die gezeigten Auflösungen des syntaktischen Zuckers können in Details von der Realität abweichen, illustrieren aber die wichtigen Punkte.

Folgendes Beispiel definiert die schreibende Schnittstelle für eine Chat-Applikation, in der sich unabhängig von der tatsächlichen Implementierung Daten senden lassen.

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

Das Übertragen in Code mit Futures offenbart ein paar Schwachstellen.

Schritt 1: Entfernen des asynchronen Schlüsselworts und Ersetzen durch eine Future.

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

Obwohl der Code auf den ersten Blick brauchbar wirkt, akzeptiert ihn der Rust-Compiler nicht. In Traits sind aktuell noch keine impl-Traits in Rückgabepositionen erlaubt. In Nightly Rust ist das zwar ein weiteres Feature, das sich bequem mit einem Makro aktivieren lässt, allerdings zum nächsten Problem führt: impl-Trait in Methoden ist ebenfalls Syntaxzucker. In Wirklichkeit erzeugt der Compiler einen assoziierten Typ:

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

Das sieht nicht allzu aufregend aus, wirft aber Licht auf eine wichtige Sache, die bisher außen vor blieb: Die Lebensdauer (Lifetime) der Future.