WebAssembly, WASI und Rust: Dreamteam für Microservices
Seite 2: Code für das Zusammenspiel
Ein einfaches Beispiel soll das Erstellen und Verwenden von Wasm Components und WASI 0.2 in Rust demonstrieren. Die Rust-Dokumentation des Wasm Component Model enthält ebenfalls eine Beschreibung eines Beispiels, das jedoch in Teilen veraltet ist. Die Dokumentation in Bezug auf Rust, Wasm Component Model und WASI 0.2 ist aufgrund der raschen technischen Entwicklung noch nicht optimal.
Um das Beispiel einfacher nachvollziehen zu können, steht das steht das Docker-Image rstropek/wasm-workshop
zur Verfügung, das alle notwendigen Werkzeuge und Bibliotheken enthält. Genauere Informationen darüber, wie das Image zu verwenden ist, findet sich in der zugehörigen Readme-Datei auf Docker Hub.
Zunächst dient cargo component
dazu, eine Wasm-Komponente zu erstellen:
cargo component new greeter --lib
Dieser Befehl erzeugt ein neues Rust-Projekt mit dem Namen greeter. Zu beachten sind folgende Besonderheiten:
- Das Projekt enthält einen Ordner wit mit der Datei world.wit. Diese enthält die Definition des Interfaces in der WIT-Sprache für die Komponente.
- In der Datei Cargo.toml des Projekts ist eine Abhängigkeit auf den Binding Generator
wit-bindgen
eingetragen. Er erstellt den Code des zu implementierenden Rust-Traits aus der WIT-Datei. Außerdem generiert er Code, der sich um technische Details der Implementierung des Abstract Binary Interface (ABI) des Wasm Component Model kümmert. - Die eigentliche Logik der Komponente ist in der Datei src/lib.rs zu implementieren.
Da sich das Beispiel auf die wichtigsten Zusammenhänge konzentriert, ist die WIT-Datei einfach gehalten:
package component:greeter@0.1.0;
interface greet {
get-greeting-message: func() -> string;
}
world example {
export greet;
}
Die zugehörige lib.rs-Datei sieht wie folgt aus:
#[allow(warnings)]
mod bindings;
use crate::bindings::exports::component::greeter::greet::Guest;
struct Component;
impl Guest for Component {
fn get_greeting_message() -> String {
"Hello, World!".to_string()
}
}
bindings::export!(Component with_types_in bindings);
Der Aufruf von cargo component build
erzeugt die Datei src/bindings.rs aus der WIT-Datei und das Wasm-Modul greeter.wasm in target/wasm32-wasip1/debug.
Diese Komponente lässt sich noch nicht ausführen. Dafür ist ein Client erforderlich, der sie einbindet. In dem Beispiel sind sowohl die Komponente als auch der Client in Rust geschrieben. Das müsste jedoch nicht so sein, sondern jede Programmiersprache, die das Wasm Component Model unterstützt, kann solche Komponenten verwenden und bereitstellen. Bei den Schnittstellen ist man nicht mehr auf die Basisdatentypen von Wasm beschränkt, sondern kann auf die Unterstützung komplexer Datentypen in WIT zurückgreifen.
Einbinden einer Wasm-Komponente
Ein weiteres Rust-Projekt dient dazu, die Wasm-Komponente in einem Rust-Programm zu verwenden. Zu beachten ist, dass diesmal das Argument --lib
entfällt.
cargo component new greeter-client
Um die zuvor erstellte Wasm-Komponente in das neue Projekt einzubinden, erhält dieses zunächst einen Ordner wit mit der Datei world.wit hinzu, die den Code zum Importieren der Komponente enthält:
package docs:app;
world app {
import component:greeter/greet@0.1.0;
}
Folgende Ergänzung am Ende der Cargo.toml-Datei des Projekts macht die neue Datei bekannt und gibt an, wo die WIT-Datei der referenzierten Komponente zu finden ist:
[package.metadata.component.target]
path = "wit"
[package.metadata.component.target.dependencies]
"component:greeter" = { path = "../greeter/wit" }
Zu beachten ist, dass der Binding Generator wit-bindgen
auch auf der Clientseite erforderlich ist. cargo component
hat ihn als Abhängigkeit in Cargo.toml eingetragen.
Die Komponente lässt sich nun in main.rs
verwenden:
mod bindings;
use bindings::component::greeter::greet::get_greeting_message;
fn main() {
println!("{}", get_greeting_message());
}
Zum Kompilieren dient erneut der Befehl cargo component build
. Das Ergebnis ist in target/wasm32-wasip1/debug zu finden. Der Versuch, das Wasm-Modul mit
wasmtime target/wasm32-wasip1/debug/greeter-client.wasm
auszuführen, schlägt fehl. Die zugehörige Fehlermeldung besagt, dass das Programm component:greeter/greet@0.1.0 benötigt, aber keine passende Implementierung gefunden wurde. Es fehlt der Schritt der sogenannten Composition, der die Imports eines Wasm-Moduls mit den Exports eines anderen zusammenführt. Dieser Mechanismus sorgt für hohe Flexibilität, da man im Nachhinein entscheiden kann, welche Implementierung man für einen Import verwenden möchte. Unter anderem lassen sich auf die Weise unterschiedliche Wasm-Module je nach Laufzeitumgebung verwenden.
Den Aufbau übernimmt das Tool WebAssembly Compositions (WAC). Beim Schreiben dieses Artikels steht in der Rust-bezogenen Dokumentation zum Wasm-Komponentenmodell für die Composition noch wasm-tools compose
. Das ist jedoch nicht mehr zeitgemäß, sondern WAC ist die aktuelle Anwendung.
wac plug target/wasm32-wasip1/debug/greeter-client.wasm \
--plug ../greeter/target/wasm32-wasip1/debug/greeter.wasm -o \
./composed-greeter.wasm
Der Befehl bindet die Komponente greeter in das Programm greeter-client ein. Das Ergebnis ist in composed-greeter.wasm zu finden. Jetzt funktioniert der Start mit
wasmtime ./composed-greeter.wasm
Die Ausgabe ist "Hello, World!".
Zugriff auf WASI 0.2
Das bisherige Beispiel demonstriert den Einsatz des Wasm-Komponentenmodells in Rust, nutzt jedoch WASI 0.2 noch nicht. Im nächsten Schritt kommt eine zweite Wasm-Komponente namens even-odd hinzu, die mithilfe von WASI 0.2 eine Zufallszahl generiert und dafür auf eine Funktion des Hosts zugreift, der im Beispiel das Betriebssystem ist.
Der Befehl cargo component new even-odd --lib
erstellt das neue Projekt. Die neue WIT-Datei erhält dann folgenden Inhalt:
package component:even-odd@0.1.0;
interface random-demo {
is-even: func() -> bool;
}
world example {
include wasi:random/imports@0.2.0;
export random-demo;
}
Beim Include von wasi:random
handelt es sich um die Interface-Definition gemäß WASI 0.2. Die entsprechenden WIT-Dateien von wasi:random
müssen aus dem GitHub-Repository von WASI-Random in das Beispielprojekt im Ordner wit/deps/random abgelegt werden. Achtung: Hier wird bewusst die Version 0.2.0 von WASI verwendet, da beim Schreiben des Artikels 0.2.1 zwar schon zur Verfügung stand, aber cargo component
noch nicht darauf vorbereitet ist.
Schließlich muss in Cargo.toml stehen, wo die WIT-Dateien von wasi:random
zu finden sind:
[package.metadata.component.target.dependencies]
"wasi:random" = { path = "wit/deps/random" }
Der Binding Generator erstellt nun die Rust-Traits zum Zugriff auf die Funktionen von wasi:random
. Dann lässt sich die Funktion get_insecure_random_u64
verwenden, die aus wasi:random
kommt und eine Zufallszahl generiert:
#[allow(warnings)]
mod bindings;
use bindings::exports::component::even_odd::random_demo::Guest;
use bindings::wasi::random::insecure::get_insecure_random_u64;
struct Component;
impl Guest for Component {
/// Say hello!
fn is_even() -> bool {
let rand = get_insecure_random_u64();
rand % 2 == 0
}
}
bindings::export!(Component with_types_in bindings);
Um das Clientprogramm zu erweitern und die neue Komponente einzubinden, die wasm:random
nutzt, muss man als erstes die Abhängigkeiten in Cargo.toml eintragen:
[package.metadata.component.target.dependencies]
"component:greeter" = { path = "../greeter/wit" }
"component:even-odd" = { path = "../even-odd/wit" }
"wasi:random" = { path = "../even-odd/wit/deps/random" }
Nun folgt der Import der neuen Komponente in der WIT-Datei:
package docs:app;
world app {
import component:greeter/greet@0.1.0;
import component:even-odd/random-demo@0.1.0;
}
Damit lässt sich in main.rs die neue Funktion nutzen:
mod bindings;
use bindings::component::greeter::greet::get_greeting_message;
use bindings::component::even_odd::random_demo::is_even;
fn main() {
println!("{}", get_greeting_message());
if is_even() {
println!("Is even");
} else {
println!("Is odd");
}
}
Vor dem Ausführen des Clients stehen das Kompilieren und der Compose-Prozess an:
cargo build component
wac plug target/wasm32-wasip1/debug/greeter-client.wasm \
--plug ../greeter/target/wasm32-wasip1/debug/greeter.wasm \
--plug ../even-odd/target/wasm32-wasip1/debug/evenodd.wasm \
-o ./composed-greeter.wasm
Wer das Programm mit wasmtime ./composed-greeter.wasm
einige Male ausführt, wird zufällig entweder Is even
oder Is odd
als Ausgabe erhalten.