Scala.js Experimental-Wasm Hello World!

by Manav

4 min read

Simple Hello-World implementation based upon their docs.

For reference my current version for sbt is 1.10.6, and the scala version is:

  > : scala version
Scala code runner version: 1.4.3
Scala version (default): 3.6.3

According to their Experimental Status, shown in Documentation.

Being experimental means that:

  • The Wasm backend may be removed in a future minor version of Scala.js (or moved to a separate plugin).
  • Future versions of Scala.js may emit Wasm that requires newer versions of Wasm engines, dropping support for older engines.

However, we do not expect the the Wasm backend to be any less correct than the JS backend, modulo the limitations listed below. Feel free to report any issue you may experience with the same expectations as for the JS backend.

Non-functional aspects, notably performance and size of the generated code, may not be as good as the JS backend for now. The backend is also not incremental yet, which means a slower fastLinkJS in the development cycle.

Requirements

The Wasm backend emits code with the following requirements:

  • A JavaScript host (i.e., we do not currently generate standalone Wasm)
  • A Wasm engine with support for:
    • Wasm 3.0
    • Wasm GC
    • Exception handling, including the latest exnref-based variant
  • The ESModule module kind (see emitting modules)
  • Strict floats (which is the default since Scala.js 1.9.0; non-strict floats are deprecated)

Supported engines include Node.js 22, Chrome and Firefox, all using some experimental flags (see below).

Implementation

val scala3Version = "3.6.3"
lazy val root = project
  .in(file("."))
  .enablePlugins(ScalaJSPlugin)
  .settings(
    name := "cats-effect-wasm-hello",
    version := "0.0.1",
    scalaVersion := scala3Version,
    
    // Configure Scala.js
    scalaJSUseMainModuleInitializer := true,
    
    // Specify the main class to use
    Compile / mainClass := Some("SimpleMain"),
    
    // Emit ES modules with the Wasm backend
    scalaJSLinkerConfig := {
      scalaJSLinkerConfig.value
        .withExperimentalUseWebAssembly(true) // use the Wasm backend
        .withModuleKind(ModuleKind.ESModule)  // required by the Wasm backend
    },
    
    // Configure Node.js to support the required Wasm features
    jsEnv := {
      import org.scalajs.jsenv.nodejs.NodeJSEnv
      val config = NodeJSEnv.Config()
        .withArgs(List(
          "--experimental-wasm-exnref",      // required
          "--experimental-wasm-imported-strings", // optional (good for performance)
          "--turboshaft-wasm"                // optional, but significantly increases stability
        ))
      new NodeJSEnv(config)
    }
  )

Above is the build.sbt which I've implemented exact same from their Docs. here Compile / mainClass := Some("SimpleMain"), points to the WASM generater Main file which is written in .scala.

which is just a simple object, and ironically it's named as SimpleMain object.

object SimpleMain {
  def main(args: Array[String]): Unit = {
    println("Hello, WebAssembly world from SimpleMain!")
  }
}


for future references

[!todo] if you want to introduce cats-effect in WASM, introduce libraryDependencies function in build.sbt.

libraryDependencies ++= Seq(
  "org.typelevel" %%% "cats-effect" % "3.5.2"
)

Compiling

  > : sbt clean
sbt fastLinkJS
[info] welcome to sbt 1.10.6 (N/A Java 21.0.5)
[info] loading settings for project hello_world-build from plugins.sbt...
[info] loading project definition from /home/chikoyeat/projects/gsoc/hello_world/project
[info] loading settings for project root from build.sbt...
[info] set current project to cats-effect-wasm-hello (in build file:/home/chikoyeat/projects/gsoc/hello_world/)
[success] Total time: 0 s, completed 12-Mar-2025, 9:37:12 pm
[info] welcome to sbt 1.10.6 (N/A Java 21.0.5)
[info] loading settings for project hello_world-build from plugins.sbt...
[info] loading project definition from /home/chikoyeat/projects/gsoc/hello_world/project
[info] loading settings for project root from build.sbt...
[info] set current project to cats-effect-wasm-hello (in build file:/home/chikoyeat/projects/gsoc/hello_world/)
[info] compiling 3 Scala sources to /home/chikoyeat/projects/gsoc/hello_world/target/scala-3.6.3/classes ...
[info] Fast optimizing /home/chikoyeat/projects/gsoc/hello_world/target/scala-3.6.3/cats-effect-wasm-hello-fastopt
[success] Total time: 4 s, completed 12-Mar-2025, 9:37:19 pm

=> build.sbt successfully compiled, which generated the main.wasm file in

  └── target
    ├── global-logging
    ├── scala-3.6.3
    │   ├── cats-effect-wasm-hello-fastopt
    │   │   ├── __loader.js
    │   │   ├── main.js
    │   │   ├── main.wasm
    │   │   └── main.wasm.map

=> Now checking this through a python server, where index.html

  <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WASM hello wrld</title>

</head>
<body>
    <div>
        <h1>WASM hello wrld</h1>
          <div id="output"></div>
    </div>
<script type="module">
    const outputDiv = document.getElementById('output');
    const originalConsoleLog = console.log;
    
    console.log = function() {
        const args = Array.from(arguments);
        originalConsoleLog.apply(console, args);
        
        const message = args.map(arg => 
            typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
        ).join(' ');
        
        outputDiv.textContent += message + '\n';
    };
    try {
        const module = await import('./target/scala-3.6.3/cats-effect-wasm-hello-fastopt/main.js');
    } catch (error) {
        console.error("Error loading WebAssembly module:", error);
    }
</script>

</body>
</html>

and the server.py for a basic python server

import http.server
import socketserver
import os

PORT = 8000

class WasmHandler(http.server.SimpleHTTPRequestHandler):
    def end_headers(self):
        # Add required headers for cross-origin isolation
        # These are needed for SharedArrayBuffer which WebAssembly might use
        self.send_header("Cross-Origin-Embedder-Policy", "require-corp")
        self.send_header("Cross-Origin-Opener-Policy", "same-origin")
        super().end_headers()
    
    def guess_type(self, path):
        if path.endswith('.wasm'):
            return 'application/wasm'
        elif path.endswith('.js'):
            return 'application/javascript'
        return super().guess_type(path)

print(f"Starting server at http://localhost:{PORT}")
print(f"Serving from directory: {os.getcwd()}")
with socketserver.TCPServer(("", PORT), WasmHandler) as httpd:
    httpd.serve_forever()

after running this server.py, index.html renders this Simple Hello World Implementation is Scala.js

pew