Compile to LLVM or WASM

Thanks for the input!

In my opinion, I think having distinct backends for Wasm in the browser and WASI could be a beneficial choice, despite the somewhat duplicated cost of implementing a compiler backend for Wasm.

  1. A WASI backend based on ScalaNative (via NIR).
  2. A Wasm for browser backend based on Scala.js.

Scala.js is incredibly stable, and its ecosystem is widely supported, making it an ideal choice for compiling to Wasm in the browser. :+1:

However, if the goal is to compile to WASI, a challenge arises. The lack of JS interop on pure WASI runtime means that we would have to reimplement everything based on WASI - including scalalib, javalib, and any third-party libraries. Such an approach seems impractical and not a viable way forward.

On the other hand, ScalaNative appears to be a good fit for WASI due to its capability in system programming. Its javalib and scalalib are developed based on libc (and posixlib).
Fortunately, wasi-libc would serve as a replacement for it on the WASI platform. (Regarding posix, let’s wait for WASI preview 2 and after, instead of WASIX :slight_smile: )

browser_wasi_shim makes it work on browser, but Scala.js based Wasm seems nicer for frontend development.

crystal’s experimental Wasm support also depends on wasi-libc.


While I acknowledge the demand for Scala on Wasm in the browser, I personally perceive that WASI opens up a broader range of possibilities for Scala.
Therefore, I am leaning towards prioritizing the compilation of Scala to WasmGC via ScalaNative initially.
This could potentially provide valuable insights that may later be applied to Wasm on browser via Scala.js.

Overall it seems to me that targetting wasmGC is the right way. It sounds like going through Scala native with a custom GC would be much slower, that would look bad when comparing Scala and Kotlin. It could also mean a higher maintenance overhead.

The question how to target WASI is important though. We already have versions of the Java library in Scala native and Scala.js. Having a third one on top of WASI sounds doable. When compiling to wasm one could choose between targetting Javascript interop or WASI (?).

It seems to me Scala is in a good position because of its wide ecosystem which already compiles to Scala.js, so it would be usable even before WebAssembly Components materialize. I don’t know if Kotlin relies more on Java libraries that would have to be made available somehow.

1 Like

Anti-jinx I guess? I just said the opposite :slight_smile: But I really don’t know. Let’s see what others think (cough sjrd cough).

Anti-jinx I guess? I just said the opposite :slight_smile: But I really don’t know. Let’s see what others think (cough sjrd cough).

Actually, I really don’t know how this decision impacts on the ecosystem in future too :smile:
I’m not sure in a long run, but, at least, the ScalaNative seems the fast path for WASI support, and Scala.js for Wasm on browser.

It sounds to me like Scala Native semantics are not a perfect match for WasmGC (unlike Scala.js semantics).

So what happens when Scalalib, Javalib, and third-party libraries depend on those semantics? This is another case where we would end up having to re-implement.

It seems to me that implementations in Scala.js and Native may fall into one of a few categories:

  1. Library code that only relies on cross-platform Scala language semantics.
  2. Scala.js library code that relies on JS semantics/interop not supported on WASI.
  3. Scala Native library code that relies on semantics not supported on WasmGC.

Suffice to say, either platform has some caveats. I don’t have a great intuition about the extent of (2) or (3).

Doable and quite possibly necessary.

It’s very hard to grasp exactly what “everything” means, because it’s not everything :slight_smile:

Also, in my third party libraries, we often don’t care what Scalalib or Javalib are doing and prefer to write our own implementations that directly use platform primitives. The performance wins can absolutely smoke anything you can do with the JDK.

I’m not an expert on WASI but if there is a way to use its primitives without shimming through wasi-libc this sounds interesting to me. This reasoning is why we opted to implement our own asynchronous I/O in Cats Effect and FS2 instead of shelling out to libuv or even worse Scala Native implementations of JDK NIO.

This is a significant endorsement. As a maintainer of foundational third-party libraries, I like when we get solid semantics, robust compiler toolchains, and comprehensive interop with platform primitives. This opens the door for us to deliver an entire library ecosystem optimized for that platform, as well as contribute upstream Javalib implementations.

2 Likes

Thanks for your opinion, the perspective of the maintainer of third-party libraries like typelevel stack is really valuable! It’s also really good to know that third-party libraries sometimes prefer to write their own implementations that directly use platform primitives.

I feel like I’ve stuck to reusing existing ecosystem/library implementations as much as possible and having WASI support, even if it’s a bit inconvenient in the short term. In the long run, however, we need to re-implement the libraries for WASI one way or another (Scala.js or ScalaNative) to have an ecosystem optimised for that platform.

Therefore, working on top of Scala.js semantics sounds legid, considering that its semantics suite well with Wasm (and WasmGC).

Sorry, I chose an wrong word :bowing_man: I meant that every “(2) Scala.js library code that relies on JS semantics/interop not supported on WASI”.

This should be possible, WASI provides a system interface (on top of the Wasm API) such as fd_write and clock_time_get. See the list of functions of WASI snapshot preview 1

(import "wasi_snapshot_preview1" "fd_write" (func (;7;) (type 5)))
(import "wasi_snapshot_preview1" "clock_time_get" (func (;4;) (type 4)))

Therefore, if we introduce a new type of annotation for interop (say @wasm.native) that compiles to the import section above, we should be able to access WASI directly from the Scala program (not via wasi_libc kind of things).

So if we want to support WASI via Scala.js semantics, Scala.js library code that relies on JS semantics/interop needs to be re-implemented using these APIs.

@wasm.native
object wasi_snapshot_preview1 {
    def fd_write(fd: Int, iovs_ptr: Int, iovs_len: Int, nwritten_ptr: Int): Int = wasm.native
    def clock_time_get(id: Int, precision: Int, time: Int): Int = wasm.native
    // ...
}

Yes, ScalaNative is not perfect because it would be difficult to lower ScalaNative’s low-level operations to WasmGC. We still need to do some reimplementation + restriction to ScalaNative, and it makes current ScalaNative libraries suboptimal, or requires complete rewrite. :disappointed:


Anyway, thank you very much for your input and spend some time on joining the discussion @armanbilge @sjrd @lrytz Let me have a bit of summarization to make sure we’re on the same page.

Supporting WasmGC based on Scala.js

  • Advantages
    • Scala.js IR is at a very good abstraction level to compile in WasmGC
    • Scala.js’s JS interop can be seamlessly reused in Wasm in the browser.
  • Disadvantages
    • In pure WASI environment (e.g. wasmtime), library code that relies on JS semantics/interop needs to be re-implemented based on WASI.

Supporting WasmGC based on ScalaNative (via NIR)

  • Advantages
    • ScalaNative library code that relies on libc can be reused in the WASI environment using wasi-libc.
    • We can choose implementations at link time without relying on any dynamic capability of the runtime.
  • Disadvantages
    • ScalaNative’s semantics, especially around low-level operations, are not well suited for WasmGC, and we’ll have to impose some restrictions on it.
    • While wasi_browser_shim makes WASI work on the browser, it wouldn’t be optimal as Wasm was developed via Scala.js.

The next step for me would be delve more into how to support WASI on top of JS ecosystem.
I’ll checkout how they work on it in kowasm (Kotlin/Wasm is developed on top of Kotlin/JS, and kowasm is developed on Kotlin/Wasm), and teavm-wasi.

(edit) Kowasm still uses JS interop. Therefore, even after wasmtime supports WasmGC, it won’t work on pure WASI runtime like wasmtime.

6 Likes

I was optimistic about supporting WasmGC through ScalaNative (NIR), but it turned out to be quite difficult (or even impossible) for several reasons.

  • ScalaNative’s C interop, especially around pointer handling and (stack) allocation. And part of the ScalaNative ecosystem relies on this C interop and cannot be ignored.
  • wasi-libc may not work well with the ScalaNative generated WasmGC code, the code needs to be adapted to the ABI tool-conventions/BasicCABI.md at main · WebAssembly/tool-conventions · GitHub
  • Also, wasm-ld may not work with the WasmGC code, we need to fork and support it.

If we want to support Wasm through ScalaNative, the Emscripten/WASI-SDK way @WojciechMazur explored last year would be realistic. (but we might need to port GC for Emscripten like Porting Boehm to Emscripten · Issue #18251 · emscripten-core/emscripten · GitHub)

I’m not sure how it goes with cycle collection in Wasm on browser or Wasm component settings though.


Overall, if we want to go with WasmGC, Scala.js is indeed the way to go.

  • Step 1: Support WasmGC in the browser
    • Users will enjoy better performing Scala.js output.
  • Step 2: Support WASI that works on Node.js
    • WasmGC code developed in Scala.js can be deployed on servers running Node.js.
  • Step 3: Support a pure WASI runtime (like wasmtime): wasmtime needs to support WasmGC, javalib and scalalib need to be rewritten based on WASI.
    • The WasmGC code will work everywhere (where the runtime supports features including WasmGC), like edge, IoT devices (hopefully), and so on. But third-party Scala.js libraries won’t work.
  • Step 4: Cross-compile Scala.js libraries that depend on JS semantics for WASI, and build the ecosystem on top of WASI.
    • Third party Scala.js libraries will work with a pure WASI environment.
  • Extra: Support for the Wasm component model (as host and guest) to interact with Wasm components developed by any other language.
4 Likes

Thank you for your extensive research and write-ups. This sounds good!

I’m hopeful that we will work on this step concurrently to steps (2) and (3) :slight_smile:

2 Likes

What about numerics? JS has only Doubles (in JS it’s called number) and Booleans, but the Doubles can act as Ints when cast to integers (e.g. num | 0). Therefore Shorts, Bytes, Floats and especially Longs have to be emulated. Emulation of Longs seems costly: scala-js/linker-private-library/src/main/scala/org/scalajs/linker/runtime/RuntimeLong.scala at ca017ea2c487140e514bbdf9d20d54b42dc5bed9 · scala-js/scala-js · GitHub (they are emulated as two Ints).

Will Scala.js compiled to WasmGC use Longs emulation or will it use 64-bit integer types provided by WASM itself? What about linking together Scala.js code compiled to JS and WASM? How Longs will be represented on both sides and translated when calling from one side to the other? What performance can we expect?

2 Likes

Those details haven’t yet explored yet, please comment on the issue if you have good suggestions. Anyway, will see how other languages like Java and Kotlin represent their primitive types in Wasm world, thank you for your comment :+1:

Regarding the performance gain, see Why Wasm? section of Add WebAssembly Linker Backend (with WasmGC and Wasm ExceptionHandling) · Issue #4928 · scala-js/scala-js · GitHub the exact number can’t be predicted though.

Sorry, I wasn’t very precise with my question, but nonetheless, it was in the context of preceding statements. I’m wondering about performance of Longs encoded by Scala.js in JS and WASM cases compared to ideal situation where native support for Longs exists everywhere and is used without any overhead.

Ideal situation (that won’t ever happen in 100%):

  • JS supports 64-bit integers natively (no overhead for computations and for storage in fields, parameters or arrays)
  • Scala.js always supports Long natively (i.e. translates them to 64-bit integers supported by underlying platform) whether it compiles to JS or WASM
  • there’s no need of translating representations of Longs when calling JS from WASM or WASM from JS

Current situation is that JS doesn’t support 64-bit integers natively (and probably won’t ever do that, except the ‘big integer’ support that can handle 64-bit integers case too, but that’s slow anyway), so it must stay as it works currently (i.e. Longs are emulated using a class RuntimeLong).

But what about encoding used for compiling Scala code to WASM and to communicate between JS and WASM? If the goal of Scala.js compiled to WASM is to integrate with other libraries compiled to WASM then probably Scala.js needs to support native 64-bit integers anyway (because they could be used by those other libraries). Glue code (that translates representations of 64-bit integers) must be present somewhere - what would be its cost?

Overall, the issue with Longs can seem like a niche one, but it definitely matters for Longs-heavy computations.

The Scala.js IR has native Longs. RuntimeLong is an implementation detail of the linker when it compiles the IR to JS. When compiling to Wasm, the linker would obviously use native Wasm i64s to implement IR Longs.

Regarding interop, in Scala.js on JS, Longs are opaque: they can be passed to JS code and they can flow back into Scala.js code, but JS code cannot otherwise manipulate them other than calling .toString() on them. If there are two separately-linked Scala.js codebases running in the same JS engine, their Longs are incompatible with each other, just like any other Scala-only type. If you separately link a Scala.js codebase to JS and one to Wasm, their Longs will also be incompatible. Wasm doesn’t change anything to this issue.

Note that you can also configure the Scala.js linker to emit bigints on JS. We don’t do it by default because it’s (much) slower than our RuntimeLong implementation.

3 Likes

Thanks for clarification.

Maybe a crazy question:

Would it be possible (or feasible or sensible) for Scala.js to emit both JS and WASM in single linking run? In other words: compile part of the project to JS and the other part to WASM (which module compiles to particular output format would be configured in build tool). This way the JS and WASM parts could cooperate freely (exchange data and call functions in both directions) without the need of exporting (externalizing?) anything. Probably this depends on the ability to import WASM module from JS module and vice versa - both would need to work to support such compilation to JS and WASM simultaneously.

  • JS project that depends on the Wasm project (JS → Wasm)
    • This makes sense, the Wasm project will emit Wasm + JS that instantiates the Wasm with the required importObject.
    • The downstream JS project can seamlessly call the exported Wasm functions via generated JS code.
  • Wasm project that depends on a project that only generates JS(?) (Wasm → JS)
    • This should be technically feasible: The downstream Wasm module can import the upstream JS project’s functionality via the import section + importOjbect when instantiating the Wasm module.
    • However, this may not be what you want? Wasm can access the external resources that are explicitly imported.
    • I’m not sure what the point is of not linking the upstream project into the wasm module.

Anyway, we would generate Wasm + JS, which would make the required JS APIs (e.g. DOM API) available to Wasm when it is instantiated.

I have a few questions.

First of all: What do you hope to achieve with compiling Scala to WASM?

I see people here talking about performance. But the reality is likely that Scala on WASM would run slower than all other platforms (at least for many years because WASM isn’t very well optimized yet, especially not for VM languages). I would expect currently a hit up to one order of magnitude. Depending how library code would be called even worse…

WASM has some very interesting properties, no question. In the long run we should support this platform for sure. But it’s not about performance.

What one should know about WASM performance:

Money quote:

On the one hand, I was happy to see that Liftoff’s output was faster than what Ignition or Sparkplug could squeeze out of JavaScript. At the same time, it didn’t sit well with me that the optimized WebAssembly module takes about 3 times as long as JavaScript.

But please read the rest. The situation is very nuanced. WASM can be fast! But only when used for the right thing in the right way.

While WebAssembly can run faster than JavaScript, it is likely that you will have to hand-optimize your code to achieve that.

But here we go: Optimizing a whole language VM is a very hard task. See how Microsoft stands currently:

https://krausest.github.io/js-framework-benchmark/2024/table_chrome_121.0.6167.86_win.html

[ STRG-F: blazor-wasm ]

(Note: “blazor-wasm-aot” is kind of like Scala Native; the other is the VM version; this clearly shows for which kind of runtime model WASM is actually better suited)

These performance considerations don’t mean WASM makes no sense at all, of course! WASM is good performance wise when you do kind of low-level “number crunching” stuff. The abstractions it offers are good at simulating a kind of very bare bones computer. (Hence the reference to ASM, a.k.a. assembly language).

Running a full VM on this abstract computer is quite wasteful. You want to run the “pure” computations.

This has also another reason: Calling into and out of your “virtual computer” is not free. You need to marshal and unmarshal all data on its way between the two worlds for “normal” calls. You could use shared memory instead, and do some zero-copy thing, but this requires again hand-optimized code.

The very next question I would rise is about library ecosystem: Scala.js links with JS libraries, and offers JS semantics where it matters, but JS libs don’t run on the WASM VM, right? So you would need to constantly call in and out when using any not Scala.js native lib. I’m not sure this is a good idea.

What I’m trying to say, I think Scala.js is not the right language variant to be compiled to WASM. It’s a VM language, it binds to JS libs, it runs most likely much faster on it’s native platform.

WASM is made for “C-like” languages. The ones that compile fine to WASM are things like Rust, C, C++, Zig. And we have also something in this space: Scala Native!

But Scala Native would for sure need a WASM GC back-end. Otherwise it will most likely also be quite underwhelming when it comes to performance.

Scala Native talks natively to languages like Rust, C, C++, Zig, thought C “ABI”. Exactly the languages which also compile to WASM… So Scala Native could use its native lib ecosystem without expensive VM to VM calls! (Besides pure Scala libs, of course).

Still this doesn’t mean that Scala.js is out of the picture. As WASM is mostly good to do some “heavy lifting” but not really optimized to do “the usual UX browser things” one needs actually both. So some interop between Scala.js and Scala-Native-compiled-to-WASM would be very welcome. It could hide the otherwise needed JS incantations to bind WASM module functions to the outside world.

All that analysis is accurate for Wasm MVP/v1. It might not be accurate anymore with WasmGC. This is why we never seriously considered compiling to Wasm before, but now that WasmGC is around the corner, we do.

2 Likes

What about the library ecosystem? Language semantics? Access to low level functionality, “C things”, like messing around with the WASM memory array directly for performance reasons?

I mean, WASM, yes sure. But it would make imho much more sense to start from Scala Native.

Like said, it’s not that Scala.js is completely out of the picture here, but it makes imho almost no sense to compile this variant to WASM. You wouldn’t get any of the WASM benefits, namely the access to a low level, almost “metal” VM, but all the drawbacks, like having to run two VMs at once, and have access to libs only though a FFI interface (which is not the fastest in case of JS objects, AFAIK).

In both cases you need to implement a custom, WASM GC backed, Scala Runtime GC. But Scala Native has already an interface for that! It offers already the feature to change GC implementations. There is nothing like that in Scala.js.

Effectively what you would do when you would port Scala.js to WASM, would be porting a “JS VM” to WASM. This makes just no sense to me. :smile:

I hope this here is not any kind of competition who has a WASM Scala implementation first. :grinning: This should be decided on the ground of technical merit.

Of course, if you have the time, port Scala.js to WASM… It would be at least an interesting experiment. Even I don’t think it had much value in practice.

Porting Scala Native would make much more sense all in all. The whole point of a super low level VM like WASM is to do low level stuff with it. Only Scala Native can do that reasonably at the moment. Like shown in the previous comment: WASM is only fast when you optimize the code. The internet is full of people wondering why their Rust modules compiled to WASM run x-times slower than equivalent JS code. That’s why it makes only sense when you can take the very specific advantages of WASM, meaning running low-level code, for “compute kernels”.

And for the use cases outside of the browsers: All the other VMs are currently superior to WASM. Running jLink things, or Graal Native images or Scala Native (for fast startup) is currently the much better option than messing around with any of the quite early days WASM stand alone runtimes.

From what you’re saying, I think you may have misconceptions about the level of abstraction that WasmGC provides.

There is only one VM, which runs JS code and WasmGC code in the same heap.

We need a little bit of bridge code, but still directly manipulating references to JavaScript objects. No need to map JS objects to integers or something like that, which I think is what you are implying with “FFI”?

No, you don’t. We can use the garbage collector of WasmGC directly. Scala objects will be allocated as WasmGC structs on its heap, and we won’t have to implement anything GC-related.

We’ll compile Scala.js code WasmGC code. No additional “VM layer” on top of that.

If this were a competition for first-to-Wasm without regard for technical applicability, we would have implemented a Scala.js-to-Wasm back-end 7-ish years ago. But like I said, Wasm v1 was not technically applicable; WasmGC shows enough promise that it’s worth trying.

2 Likes