scala-wasm
scala-wasm is a friendly fork of Scala.js, targeting stand-alone Wasm runtimes such as wasmtime, leveraging the Wasm Component Model.
Links
- scala-wasm on GitHub
- wit-bindgen-scala
- component-examples
- wasi4s
- scala-js-env-wasmtime
- Spin templates
Installation
sbt plugin
Current version: 1.22.1-wasm.6
Add the plugin in project/plugins.sbt:
addSbtPlugin("io.github.scala-wasm" % "sbt-scalajs" % "1.22.1-wasm.6")
Prerequisites
Configuration
scala-wasm adds WIT binding generation and Wasm component linking on top of sbt-scalajs.
project/build.sbt
WasmtimeEnv from wasmtime-env runs linked components with wasmtime when you sbt run or sbt test. Add the following to project/build.sbt:
libraryDependencies +=
"io.github.scala-wasm" %% "scalajs-env-wasmtime" % "0.1.1"
build.sbt
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(
scalaJSLinkerConfig ~= {
_.withESFeatures(_.withUseWebAssembly(true).withESVersion(ESVersion.ES2022))
.withModuleKind(ModuleKind.WasmComponent) // emit Wasm Component
.withWasmFeatures(_.withModuleInitializerExport(Some(
// (optional) Export main as wasi:cli/run (WASI entrypoint)
WasmComponentModuleInitializerExport(
scope = WitScope.Interface("wasi", "cli", "run", Some("0.2.0")),
functionName = "run",
resultType = ResultType.ResultUnitUnit,
))))
},
// (optional) `wit` directory
// If `world.wit` exists, scala-wasm automatically generate
// WIT bindings using `wit-bindgen-scala`.
scalaJSWitDirectory := baseDirectory.value / "wit",
scalaJSWitWorld := Some("command"),
scalaJSWitPackage := Some("example"),
// (optional) if you wanna export main as endpoint
scalaJSUseMainModuleInitializer := true,
// so that we can run/test on wasmtime.
jsEnv := new WasmtimeEnv(),
)
WasmtimeEnv needs wasmtime on PATH. Component tests that use the Scala.js test RPC bridge also need wac — see scalajs-env-wasmtime.
Example: Hello world (WASI).
Hello world (WASI)
This tutorial walks through a minimal Hello, world app that uses WASI APIs and runs on Wasmtime. You build a command component that exports the WASI entrypoint wasi:cli/run, then run it with sbt run or wasmtime run.
This example uses wasi4s, which is a pre-generated WASI bindings, to call WASI from Scala. 1
For a complete project example, see component-examples, the helloworld/ subdirectory and the build.sbt.
Project layout
helloworld-wasi/
build.sbt
project/
plugins.sbt
build.properties
build.sbt
src/main/scala/
Main.scala
project/
// project/plugins.sbt
addSbtPlugin("io.github.scala-wasm" % "sbt-scalajs" % "1.22.1-wasm.6")
// project/build.properties
sbt.version=2.0.7
// project/build.sbt
// https://github.com/scala-wasm/scala-js-env-wasmtime
// runs the linked component on wasmtime when you sbt run
libraryDependencies +=
"io.github.scala-wasm" %% "scalajs-env-wasmtime" % "0.1.1"
build.sbt
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 := "helloworld-wasi",
version := "0.1.0-SNAPSHOT",
scalaVersion := "2.13.18",
libraryDependencies +=
"io.github.scala-wasm" %%% "wasi4s" % "0.1.0+wasi-0.2.12",
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(),
)
withUseWebAssemblyandwithModuleKind(ModuleKind.WasmComponent)tell the linker to emit a Wasm component.scalaJSUseMainModuleInitializer := trueplaces yourmainas a Wasm module initializer.withModuleInitializerExportexports that initializer aswasi:cli/run@0.2.12.
src/main/scala/Main.scala
package example
import io.github.scalawasm.wasi4s.wasi.cli.stdout._
object Main {
def main(args: Array[String]): Unit = {
val out = getStdout()
out.blockingWriteAndFlush("Hello, world!\n")
}
}
This example writes to the host using WASI’s low-level stdout API. 2
Run
sbt fastLinkJS
[info] Fast optimizing .../component-examples/target/out/sjs1/scala-2.13.18/helloworld/helloworld-fastopt
main.wasm is generated in that directory. If build.sbt sets jsEnv := new WasmtimeEnv(), then sbt run runs main.wasm using wasmtime from sbt.
sbt run
[info] Running wasi:cli/run.
Hello, world!
To run the artifact yourself using wasmtime:
wasmtime run -W function-references,gc,exceptions path/to/main.wasm
(-W flags might be no longer needed with the latest version of wasmtime).
-
You can instead add a project
wit/directory withworld.witthat imports WASI explicitly and generate bindings withwit-bindgen-scala. We'll coverworld.witin another tutorial. ↩ -
Why not
println? scala-wasm does not ship aprintlnimplementation today; you define stdout yourself via WASI (or a small wrapper). See Limitations. ↩
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 \ (/
'_ - _'
/ '-----' \
Hello WASI HTTP server
To build an HTTP server on WASI, we implement and export the wasi:http/incoming-handler interface. Runtimes such as wasmtime serve and WASI-aware Kubernetes shims call that export to dispatch each incoming HTTP request to our component.
This tutorial shows how to export a WIT interface from Scala. The example handles GET /health with JSON.
Related examples are available at:
- outbound HTTP: wasi-http-client
- Spin deployment WASI http server: http-scala-wasm-health.
Project layout
http-health/
build.sbt
project/
plugins.sbt
build.properties
wit/
world.wit
src/main/scala/httphealth/
Server.scala
ServerHandler.scala
wit/world.wit
As always, write an WIT world for our component.
package example:http-health;
world health {
export wasi:http/incoming-handler@0.2.12;
}
Run wkg wit fetch from the project directory to download the WIT files this world depends on (into wit/deps/).
build.sbt
import org.scalajs.linker.interface.ESVersion
lazy val root = project
.in(file("."))
.enablePlugins(ScalaJSPlugin)
.settings(
name := "http-health",
scalaVersion := "2.13.18",
libraryDependencies +=
"io.github.scala-wasm" %% "wasi4s" % "0.0.1+wasi-0.2.12",
scalaJSWitDirectory := baseDirectory.value / "wit",
scalaJSWitWorld := Some("health"),
scalaJSWitPackage := Some("httphealth"),
scalaJSUseMainModuleInitializer := false,
scalaJSLinkerConfig ~= {
_.withESFeatures(_.withUseWebAssembly(true).withESVersion(ESVersion.ES2022))
.withModuleKind(ModuleKind.WasmComponent)
},
)
Server.scala
For a WIT export, wit-bindgen-scala does not write handler code into managed sources. Instead, sbt scalaJSGenerateWitBindings (also run as part of compile) prints stub definitions in the log. Copy them into a Scala object.
[info] Export stubs for `wasi:http/incoming-handler@0.2.12` (copy into a static object and implement):
[info] @scala.scalajs.wit.annotation.WitExport(scala.scalajs.wit.annotation.WitScope("wasi", "http", "incoming-handler", "0.2.12"), "handle")
[info] def handle(@scala.scalajs.wit.annotation.WitName("request") request: IncomingRequest, @scala.scalajs.wit.annotation.WitName("response-out") responseOut: ResponseOutparam): Unit = ???
package httphealth
import io.github.scalawasm.wasi4s.wasi.http.types.{IncomingRequest, ResponseOutparam}
import scala.scalajs.wit.annotation.{WitExport, WitName, WitScope}
object Server {
@WitExport(WitScope("wasi", "http", "incoming-handler", "0.2.12"), "handle")
def handle(
@WitName("request") request: IncomingRequest,
@WitName("response-out") responseOut: ResponseOutparam): Unit =
ServerHandler.handle(request, responseOut)
}
ServerHandler.scala
package httphealth
import scala.scalajs.{wit => wm}
import scala.scalajs.wit.unsigned.UByte
import io.github.scalawasm.wasi4s.wasi.http.types._
import scala.util.control.NonFatal
object ServerHandler {
def handle(request: IncomingRequest, outParam: ResponseOutparam): Unit = {
try {
val path = request.pathWithQuery().getOrElse("/").takeWhile(_ != '?')
request.method() match {
case Method.Get if path == "/health" =>
send(outParam, 200, "application/json", """{"status":"ok"}""")
case _ =>
send(outParam, 404, "text/plain; charset=utf-8", "not found")
}
} catch {
case NonFatal(err) =>
send(outParam, 500, "text/plain; charset=utf-8", err.toString())
}
}
private def send(outParam: ResponseOutparam, status: Int,
contentType: String, responseBody: String): Unit = {
val headers = Fields.fromList(Array(
wm.Tuple2("content-type", contentType.getBytes().asInstanceOf[Array[UByte]])
)).getOrElse(Fields())
val response = OutgoingResponse(headers)
val payload = responseBody.getBytes().asInstanceOf[Array[UByte]]
(for {
_ <- response.setStatusCode(status.toShort)
body <- response.body()
_ = ResponseOutparam.set(outParam, new wm.Ok(response))
out <- body.write()
_ <- out.blockingWriteAndFlush(payload).mapErr(_ => ())
_ = out.close()
_ <- OutgoingBody.finish(body, wm.None).mapErr(_ => ())
} yield ()).getOrElse(throw new Error("failed to send response"))
}
}
Run
Link the component:
sbt fastLinkJS
Serve with Wasmtime (flags depend on your Wasmtime version; see running HTTP components):
wasmtime serve -W function-references,gc,exceptions path/to/main.wasm
In another terminal:
curl -s http://127.0.0.1:8080/health
{"status":"ok"}
More examples
Limitations
scala-wasm targets Wasm components and WASI hosts.
No JavaScript interop available
ModuleKind.WasmModule and ModuleKind.WasmComponent cannot depend on JavaScript. The linker analyzer rejects JS interop if it's reachable:
[error] Uses JS interop with with a Wasm-only module kind:
[error] at file:/.../HelloWorld.scala:7:34: global:console["log"](...)
[error] at file:/.../HelloWorld.scala:7:16: global:console
[error] called from static example.Main.main([java.lang.String)void
[error] called from core module module initializers
[error] There were linking errors
Libraries built with an older Scala.js could hit this even when your code never calls scala.scalajs.js.1
To cross compile JS and a no-JS Wasm implementation, branch with scala.scalajs.LinkingInfo.linkTimeIf. See linkTimeIf.
No System.* (println, random, currentTimeMillis)
println, Math.random, and System.currentTimeMillis are implemented with JavaScript (console.log, Math.random, Date.now). The analyzer rejects them the same way:
[error] Uses JS interop with with a Wasm-only module kind:
[error] at .../java/lang/System.scala:463:27: global:console["log"](line)
[error] called from scala.Predef$.println(java.lang.Object)void
[error] Uses JS interop with with a Wasm-only module kind:
[error] at .../java/lang/Math.scala:376:54: global:Math["random"]()
[error] called from static java.lang.Math.random()double
[error] Uses JS interop with with a Wasm-only module kind:
[error] at .../java/lang/System.scala:74:16: global:Date["now"]()
[error] called from static java.lang.System.currentTimeMillis()long
On the Component Model, we need to reimplement those functions based on WASI.
In future, we might want to make System pluggable with WASIp2/WASIp3 based implementation like #246
No Scala 3
scala-wasm haven't yet supported Scala 3.2
Does XXX library work on Wasm Component?
There is no way to tell that a library supports Wasm components until linktime. Those JS interop linking errors show up only at a link error, and currently there's no way to tell which library perfectly works on Wasm Component at earlier phases. 3
-
For example, before Scala.js 1.20.1, it compiles varargs with
js.Array. And before 1.19.0 lambdas were compiled as JavaScript arrow functions. For example, cats 2.13.0 is built with Scala.js 1.18, so it may fail to link. ↩ -
because the scala-wasm API is still not stable. The Scala 3 compiler for Scala.js lives in the scala3 repository, not in the Scala.js repository. So for scala-wasm to support Scala 3, support for scala-wasm would have to be implemented on the scala3 side, but features that haven't yet been upstreamed to Scala.js shouldn't be added to scala3. Of course, we could implement and provide a forked Scala 3 compiler, but the maintenance cost of a fork is high, and we don't plan to do that until Wasm Component support is upstreamed to Scala.js. ↩
-
I'm often asked whether we'll implement WASIp2 or WASIp3 as new compile targets like SJSIR, native, and JVM, but we have no plans to do so at the moment. The benefit of adding a new compile target is that, users could tell whether a library supports (or claims to support) WASIp2 at library resolution. However, splitting compile targets this way would increase the number of artifacts uploaded to Maven, and libraries that have no JS dependencies (which should work out of the box) would also need to be rebuilt for Wasm. And also, although WASI is currently the main system API, Wasm can define arbitrary system interfaces (such as WASIX and WALI), and it isn't realistic to cover all of them with compile targets. So, I'm not planning to define WASIpX as separate compile target. ↩
WIT type mappings
wit-bindgen-scala maps WIT types in your world to Scala types. Generated imports live under {base-package}.{namespace}.{package}; exports under {base-package}.exports.{namespace}.{package}.
Scalar and composite types
| WIT | Scala |
|---|---|
bool | Boolean |
s8 | Byte |
u8 | scala.scalajs.wit.unsigned.UByte |
s16 | Short |
u16 | scala.scalajs.wit.unsigned.UShort |
s32 | Int |
u32 | scala.scalajs.wit.unsigned.UInt |
s64 | Long |
u64 | scala.scalajs.wit.unsigned.ULong |
f32 | Float |
f64 | Double |
char | Char |
string | String |
list<T> | Array[T] |
option<T> | scala.scalajs.wit.Option[T] |
result<T, E> | scala.scalajs.wit.Result[T, E] |
tuple<T1, T2> | scala.scalajs.wit.Tuple2[T1, T2] |
Unsigned integers use scala.scalajs.wit.unsigned so they can be distinct from signed Scala numeric types at the ABI boundary.
User-defined types
| WIT | Scala |
|---|---|
record | final class with @WitRecord |
variant | sealed trait with @WitVariant, final class for cases |
enum | sealed trait with @WitEnum, object for cases |
flags | final class wrapping Int |
resource (import) | opaque final class with @WitResourceImport |
Imports, exports, and resources
Import function
WIT (world scala imports this interface; see hello component model):
package scala-wasm:rust-compose;
interface greeter {
greet: func(name: string) -> string;
}
Generated Scala (in the greeter package object under managed sources):
import scala.scalajs.wit.annotation.{WitImport, WitScope, WitName}
import scala.scalajs.wit
@WitImport(
WitScope.unversioned("scala-wasm", "rust-compose", "greeter"), "greet")
def greet(
@WitName("name") name: String): String = wit.native
Export function
Same greeter interface, but your world exports it.
WIT:
world my-server {
export greeter;
}
Stub (scope matches your package and interface):
import scala.scalajs.wit.annotation.{WitExport, WitScope, WitName}
import scala.scalajs.wit
object GreeterExport {
@WitExport(
WitScope.unversioned("scala-wasm", "rust-compose", "greeter"), "greet")
def greet(@WitName("name") name: String): String = s"Hello, $name!"
}
Imported resource
WIT:
interface counters {
resource counter {
constructor(initial: s32);
increment: func();
get: func() -> s32;
}
}
Generated Scala:
@WitResourceImport(scope, "counter")
final class Counter private () extends Object {
@WitResourceMethod("increment")
def increment(): Unit = scala.scalajs.wit.native
@WitResourceMethod("get")
def get(): Int = scala.scalajs.wit.native
@WitResourceDrop
def close(): Unit = scala.scalajs.wit.native
}
object Counter {
@WitResourceConstructor
def apply(initial: Int): Counter = scala.scalajs.wit.native
}
Use val c = Counter(0); c.increment(); c.get(); c.close(). Resource exports are not generated.
Record, variant, and result
interface catalog {
record item {
sku: string,
tags: list<string>,
}
variant money { cash(u32), none }
enum status { ok, not-found }
lookup: func(sku: string) -> result<item, status>;
}
@WitRecord(scope, "item")
final class Item(
@WitName("sku") val sku: String,
@WitName("tags") val tags: Array[String])
@WitVariant(scope, "money")
sealed trait Money
object Money {
@WitName("cash")
final class Cash(@WitName("value") val value: scala.scalajs.wit.unsigned.UInt) extends Money
@WitName("none")
object None extends Money
}
@WitEnum(scope, "status")
sealed trait Status
object Status {
@WitName("ok") object Ok extends Status
@WitName("not-found") object NotFound extends Status
}
@WitImport(scope, "lookup")
def lookup(@WitName("sku") sku: String): scala.scalajs.wit.Result[Item, Status] =
scala.scalajs.wit.native
Naming
- Types:
kebab-case→PascalCase - Functions and fields:
kebab-case→camelCase - Packages:
kebab-case→snake_case1 - Scala keywords: backticks (e.g.
`type`)
Generator limits
- Resource exports are not supported (import only).
- Futures and streams are not supported yet.
-
I don't like it, this might change in the future. ↩
linkTimeIf
How do you keep a JavaScript implementation and a Wasm component implementation in one artifact?
scala.scalajs.LinkingInfo.linkTimeIf chooses a branch at link time, and the unused branch never be linked.
import scala.scalajs.LinkingInfo.{linkTimeIf, moduleKind, ModuleKind}
linkTimeIf(moduleKind == ModuleKind.WasmComponent) {
wasiPrintln(s)
} {
js.Dynamic.global.console.log(s)
}
linkTimeIf(cond)(thenBranch)(elseBranch) has the same shape as if. cond may only use
see https://www.scala-js.org/news/2025/09/06/announcing-scalajs-1.20.1/ for more details.
For example, from java.lang.Float in scala-wasm. MinimalWasmModule and WasmComponent parse without js.Dynamic.global.parseFloat, and every other module kind uses it.
import scala.scalajs.LinkingInfo.{linkTimeIf, moduleKind}
import scala.scalajs.LinkingInfo.ModuleKind.{MinimalWasmModule, WasmComponent}
linkTimeIf[scala.Float](moduleKind == MinimalWasmModule || moduleKind == WasmComponent) {
parseFloatDecimalWasm(fullNumberStr, integralPartStr, fractionalPartStr, exponentStr)
} {
parseFloatDecimalJS(fullNumberStr, integralPartStr, fractionalPartStr, exponentStr)
}