Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Implementing and Evaluating a High-Level Langua...

Implementing and Evaluating a High-Level Language with WasmGC and the Wasm Component Model: Scala’s Case

WebAssembly Workship @ ICFP/SPLASH 2025 https://conf.researchr.org/home/icfp-splash-2025/webassembly-ws-2025#program

There are proposals to extend WebAssembly (Wasm) for assisting implementations of high-level languages. The prominent ones are WebAssembly Garbage Collection (WasmGC) and the Wasm Component Model, which provides a garbage collection feature and language interoperability, respectively. This presentation reports the first serious compiler implementation that exploits both WasmGC and the Wasm Component Model. We implemented a Scala to Wasm compiler and evaluated its performance. Compared to the compilers targeting JavaScript and JVM, our compiler generates code that peforms up to 3.4 times faster and 3.5 times slower, respectively. Foreign function calls are, compared to the Rust-to-Wasm compiler, slower by more than a factor of 4 due to memory copy overhead.

Avatar for Rikito Taniguchi

Rikito Taniguchi

October 20, 2025
Tweet

More Decks by Rikito Taniguchi

Other Decks in Technology

Transcript

  1. 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
  2. Scala compilation pipelines Scala.js: Scala to JS compiler, now targeting

    Wasm as well. ✅ JS embedding WIP: non-JS embedding/WASI 2/18
  3. 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
  4. 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
  5. 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
  6. 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
  7. 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
  8. 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
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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