Slide 1

Slide 1 text

Implementing and Evaluating a High-Level Language with WasmGC and the Wasm Component Model: Scala’s Case WebAssembly Workshop @ ICFP/SPLASH 2025 October 16, 2025 Rikito Taniguchi (VirtusLab and Institute of Science Tokyo) Sébastien Doeraene (EPFL) Hidehiko Masuhara (Institute of Science Tokyo) 1/18

Slide 2

Slide 2 text

Scala compilation pipelines Scala.js: Scala to JS compiler, now targeting Wasm as well. ✅ JS embedding WIP: non-JS embedding/WASI 2/18

Slide 3

Slide 3 text

JVM baseline Node.js 24.10.0 Scala.js 1.20.1 OpenJDK Zulu24.28+83-CA Mac OS Sequoia 15.5 Apple M2 3/18

Slide 4

Slide 4 text

Compiler design and implementation 4/18

Slide 5

Slide 5 text

Object Model (type $c.Point2D (sub $c.Point (struct (field $vtable (ref $v.Point2D)) (field $f.Point2D.x (mut f64)) (field $f.Point2D.y (mut f64))))) (type $v.Point2D (sub $v.Point (struct ;; class information ;; itables (field $m.Point2D.getClass (ref $1)) (field $m.Point2D.hashCode (ref $2)) (field $m.Point2D.toString (ref $3))))) abstract class Point class Point2D(val x: Double, val y: Double) extends Point (type $3 (func (param (ref any)) (result externref))) 5/18

Slide 6

Slide 6 text

Virtual Method Call $vtable $f.Point2D.x $f.Point2D.y (local $p2d (ref null $c.Point2D)) local.get $p2d br_on_null $1 ;; jump to throw if null local.tee $tmp ;; for parameter of toString local.get $tmp struct.get $c.Point2D $vtable struct.get $v.Point2D $m.Point2D.toString call_ref $3 $getClass $hashCode $toString (type $3 (func (param (ref any)) (result externref))) Get the function reference to toString from the vtable Interface Method call has one more indirection 6/18

Slide 7

Slide 7 text

Calling JS function makeArray2(1, 2) def makeArray2(x: Int, y: Int): js.Array[Int] = js.Array(x, y) (func $f.makeArray2 (param $x i32) (param $y i32) (result anyref) ;; js Array local.get $x local.get $y call $customJSHelper.1) // JavaScript helper "1": ((x, y) => [x, y]), (import "__scalaJSCustomHelpers" "1" (func $customJSHelper.1 (param i32) (param i32) (result anyref))) [1]: except Long and Char. Scala.js guarantees its primitive types map to JS primitives [1] even when it is upcast or used in a generic context. JS numbers 7/18

Slide 8

Slide 8 text

Boxing and JS interop makeArray2(1, 2) def makeArray2[T](x: T, y: T): js.Array[T] = js.Array(x, y) Scala.js guarantees its primitive types map to JS primitives [1], even when it is upcast or used in a generic context. // JavaScript helper "1": ((x, y) => [x, y]), (import "__scalaJSCustomHelpers" "1" (func $customJSHelper.1 (param anyref) (param anyref) (result anyref))) Should be JS numbers [1]: except Long and Char. (func $f.makeArray2 (param $x anyref) (param $y anyref) (result anyref) ;; js Array local.get $x local.get $y call $customJSHelper.1) 8/18

Slide 9

Slide 9 text

i32 isn’t subtype of anyref, box i32 to anyref i32.const 1 call $boxInt i32.const 2 call $boxInt call $f.makeArray2 Boxed integer should also be mapped to JS number (func $boxInt (param $x i32) (result (ref any)) How do we box primitives? ;; class java.lang.Integer(value: Int) (type $c.j.l.Integer (sub $c.j.l.Object (struct ;; ... (field $f.value (mut i32))))) ❌ Box by struct? It’s opaque to JS 9/18

Slide 10

Slide 10 text

Boxing/Unboxing to JS primitives (import "__scalaJSHelpers" "boxInt" (func $boxInt (param i32) (result anyref))) (import "__scalaJSHelpers" "unboxInt" (func $unboxInt (param anyref) (result i32))) // JavaScript helper boxInt: (x) => x, unboxInt: (x) => x, Reference to JS number but calling JS from Wasm is expensive Convert between i32 ⇔ JS number 10/18

Slide 11

Slide 11 text

Box integers using i31ref (func $bI (param $x i32) (result (ref any)) ;; ... ;; check if $x fits in ;; 31-bit signed integer if (result (ref any)) local.get $x call $boxInt else local.get $x ref.i31 end) If it doesn’t fit in 31bit int, use JS number If it’s small enough, use i31ref. No Wasm-to-JS call Boxing without crossing Wasm/JS boundary. i31 values are seen as JS numbers from JS host. https://www.w3.org/TR/wasm-js-api/#tojsvalue 11/18

Slide 12

Slide 12 text

JS Primitive Builtins Proposal (Phase 1) Wasm builtin functions for manipulating JS primitives, efficiently callable without Wasm-to-JS overhead (similar to JS String Builtins proposal). "wasm:js-number" "fromI32" func fromI32(x: i32) -> (ref extern) "wasm:js-number" "toI32" func toI32(x: externref) -> i32 "wasm:js-number" "testI32" func testI32(x: externref) -> i32 https://github.com/WebAssembly /js-primitive-builtins 12/18

Slide 13

Slide 13 text

Non-JS embeddings, Wasm Component Model 13/18

Slide 14

Slide 14 text

Wasm Component Model ‘w’ ‘a’ ‘s’ ‘m’ Linear Memory ‘w’ ‘a’ ‘s’ ‘m’ Linear Memory Copy by VM Component A Component B Component: a new binary format that encapsulates core Wasm modules. Shared-nothing architecture: components do not share memory. VM copies data between linear memories of each component (for arguments and return value). 14/18

Slide 15

Slide 15 text

Wasm Component Model + WasmGC ‘w’ ‘a’ ‘s’ ‘m’ Linear Memory Copy (by producer) (array i16) “wasm” ‘w’ ‘a’ ‘s’ ‘m’ Linear Memory Copy (by producer) (array i16) “wasm” Copy by VM WasmGC objects cannot be passed directly to other components. Data must be copied to linear memory on the component boundary (for arguments and return values). Component A Component B 15/18

Slide 16

Slide 16 text

Benchmarking Component Model Function Call Overhead Byte Size Scala Mean (ns) Rust Mean (ns) Scala SD (ns) Rust SD (ns) 20 945 184 320 50 200 1,994 487 187 47 2,000 22,269 3,573 748 212 20,000 125,425 29,035 4,631 3,183 Benchmarking the identity function (string -> string) call between ● Scala ⇔ Scala (WasmGC) ● Rust ⇔ Rust (non-WasmGC) 16/18

Slide 17

Slide 17 text

Optimizing Component Model Calls in WasmGC? (in future) ● Discussion on WasmGC Object Support in the Canonical ABI? ○ https://github.com/WebAssembly/component-model/issues/525 ● Bulk copy operation between linear memory and WasmGC arrays? ○ Currently, there’s no way to do that, and copying element-by-element using load and store. ○ but It's only useful for string calls in the Component Model. Open question 17/18

Slide 18

Slide 18 text

Conclusion ● Scala to Wasm compiler using WasmGC ○ https://github.com/scala-js/scala-js (JS embedding) ○ https://github.com/scala-wasm/scala-wasm (WIP non-JS embedding) ● Wasm is up to over 3x faster than JavaScript ● Potential causes of the slowdown ○ Wasm-to-JS call for boxing (JS primitive builtins) ○ and? ● Slower Component Model call with WasmGC ○ WasmGC ⇔ Linear Memory copy overhead https://github.com/WebAssembly/js-primit ive-builtins 18/18

Slide 19

Slide 19 text

Backup Slides

Slide 20

Slide 20 text

Non-JS embedding JS embedding Non-JS embedding String Representation JS string + JS String Builtins Linked list around (array i16) Boxing/Unboxing JS primitive + i31ref I31ref + Box class class IntBox(value: Int) … JDK API impl Based on JS interop Regex, Math, Variable-Length Array, Encode/Decode decimal float, System APIs, etc WASI preview2 + reimplement in Scala