Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Hello world (Component Model)

In Hello world (WASI) we built a Wasm component with WASI APIs and ran it on Wasmtime.

This tutorial composes Scala with a Rust guest built for wasm32-wasip2: Rust exports a custom greeter API, and Scala imports it, and wac plugs the two .wasm artifacts into one runnable component.

Complete project available at component-examples/rust-compose.

Prerequisites

Installation, plus a Rust toolchain with the wasm32-wasip2 target (rustup target add wasm32-wasip2).

Project layout

The reference lives under component-examples/rust-compose (the repo also defines rustComposeScala in its root build.sbt):

rust-compose/
  wit/
    world.wit
  scala/
    build.sbt
    src/main/scala/rustcompose/
      RunImpl.scala
  rust-greeter/
    Cargo.toml
    src/
      lib.rs # wit-bindgen reads ../wit

WIT

First, write a WIT (Wasm Interface Type) file shared by the Scala and Rust. For more details about WIT, please refer https://component-model.bytecodealliance.org/design/wit.html.

In interface greeter, define greet function that takes a string and returns a string. That is the API the two languages call across the component boundary.

Next, declare the world each component implements. In world scala, import greeter, and in world rust, export greeter. wac plug will compose Rust’s export to Scala’s import.

package scala-wasm:rust-compose;

interface greeter {
  greet: func(name: string) -> string;
}

world scala {
  import greeter;
}

world rust {
  export greeter;
}

Run wkg wit fetch from the project directory to download the WIT files this world depends on (into wit/deps/).

Scala component

Implement the Scala side first, calls greet and prints the result.

Similar to Hello world (WASI), WasmComponentModuleInitializerExport exports main as wasi:cli/run@0.2.12.

import org.scalajs.ir.WitScope
import org.scalajs.jsenv.wasmtime.WasmtimeEnv
import org.scalajs.linker.interface.ESVersion
import org.scalajs.linker.interface.WasmComponentModuleInitializerExport
import org.scalajs.linker.interface.WasmComponentModuleInitializerExport._

lazy val root = project
  .in(file("."))
  .enablePlugins(ScalaJSPlugin)
  .settings(
    name := "rust-compose-scala",
    scalaVersion := "2.13.18",

    libraryDependencies +=
      "io.github.scala-wasm" %%% "wasi4s" % "0.0.1+wasi-0.2.12",

    scalaJSWitDirectory := baseDirectory.value / "../wit",
    scalaJSWitWorld := Some("scala"),
    scalaJSWitPackage := Some("rustcompose"),

    scalaJSLinkerConfig ~= {
      _.withESFeatures(_.withUseWebAssembly(true).withESVersion(ESVersion.ES2022))
        .withModuleKind(ModuleKind.WasmComponent)
        .withWasmFeatures(_.withModuleInitializerExport(Some(
          WasmComponentModuleInitializerExport(
            scope = WitScope.Interface("wasi", "cli", "run", Some("0.2.12")),
            functionName = "run",
            resultType = ResultType.ResultUnitUnit,
          ))))
    },

    scalaJSUseMainModuleInitializer := true,

    jsEnv := new WasmtimeEnv(),
  )

scalaJSWitDirectory, scalaJSWitWorld, and scalaJSWitPackage point at ../wit and world scala. On compile, the plugin runs wit-bindgen-scala (bindings under managed sources) and passes WIT settings to the linker.

src/main/scala/rustcompose/RunImpl.scala:

package rustcompose

import io.github.scalawasm.wasi4s.wasi.cli.stdout._
import rustcompose.scala_wasm.rust_compose.greeter.greet
import scala.scalajs.wit.unsigned.UByte

object RunImpl {
  def main(args: Array[String]): Unit =
    println(greet("Scala"))

  private def println(s: String): Unit = {
    val out = getStdout()
    try {
      val bytes = (s + "\n").getBytes().asInstanceOf[Array[UByte]]
      out.blockingWriteAndFlush(bytes).getOrElse(
        throw new RuntimeException("Failed to write to stdout"))
    } finally {
      out.close()
    }
  }
}

Rust greeter

Next, implement the Rust component. We woundn't explain details, but wit-bindgen reads ../wit and generates bindings for world rust. And we implement greeter on the generated Guest trait. Pass the incoming name to ferris-says and return the formatted string.

rust-greeter/src/lib.rs:

#![allow(unused)]
fn main() {
mod bindings {
    wit_bindgen::generate!({
        path: "../wit",
        world: "rust",
    });

    use super::Component;
    export!(Component);
}

use bindings::exports::scala_wasm::rust_compose::greeter::Guest;

struct Component;

impl Guest for Component {
    fn greet(name: String) -> String {
        let mut buffer = Vec::new();
        let message = format!("Hello from Rust, {name}!");
        ferris_says::say(&message, 80, &mut buffer).unwrap();
        String::from_utf8(buffer).unwrap()
    }
}
}

Build and run

cd scala
sbt fastLinkJS
cd rust-greeter
cargo build --target wasm32-wasip2 --release

Plug Rust into Scala, then run with Wasmtime:

cd rust-compose
wac plug --plug rust-greeter/target/wasm32-wasip2/release/rust_greeter.wasm \
  ../target/path/to/main.wasm \
  -o out.wasm
wasmtime -W gc,function-references,exceptions out.wasm
 _________________________
< Hello from Rust, Scala! >
 -------------------------
        \
         \
            _~^~^~_
        \) /  o o  \ (/
          '_   -   _'
          / '-----' \