-
Notifications
You must be signed in to change notification settings - Fork 397
Description
Motivation
The JavaScript Promise Integration proposal (JSPI) for WebAssembly adds a superpower to WebAssembly, compared to JavaScript. It allows Wasm code to perform synchronous calls to Promise
-returning JS functions, and suspend when the Promise
is not resolved yet. This is a power basically equivalent to Virtual Threads (aka green threads, coroutines, etc.), something that is not possible to implement with JavaScript alone. Basically a non-blocking await(promise)
function.
I successfully leveraged JSPI with Scala.js-on-Wasm in a hobby project to dramatically simplify the internal API :
sjrd/funlabyrinthe-scala@18bc1f2
However, to achieve it, I had to textually post-process the main.js
file generated by Scala.js, hacking and slashing its abstractions to build my own. I used my internal knowledge of how the linker generates code to do this. This is not something we're supposed to do, obviously, and it will likely break when I next uprade Scala.js.
It would be nice to have dedicated language support to leverage JSPI from Scala.js code.
Proposal
There are two sides to JSPI : importing a WebAssembly.Suspending
function, and wrapping an export into a WebAssembly.promising
call.
Promising Exports
We need a way to express that an exported def
must be wrapped in a WebAssembly.promising
call. Note that the Wasm function f
itself must be given to promising(f)
. Currently Scala.js wraps every exported function in a JavaScript-defined function
to protect its abstractions. This is why we cannot currently call promising
in user-space.
I propose a distinct @JSExportPromising
annotation, to be used like @JSExportTopLevel
on def
s:
object Container {
@JSExportPromising("name", "module.js")
def myPromisingMethod(x: Int, y: String): String = ???
}
As seen from JavaScript, it would return a js.Promise[String]
, following the API transformation applied by WebAssembly.promising
.
Suspending Imports
On the other side, we need a way to express that we import a JS function as wrapped into a new WebAssembly.Suspending(f)
constructor. Here as well, the resulting Suspending
instance must be directly import
ed into Wasm. Since Scala.js generates custom arrow functions for all imports to protect its abstractions, once again this cannot be achieved in user-space.
I see two possibilities here.
The first one is simpler from the Scala.js language specification point of view. The second one is closer to the JSPI API design.
First option: offer a single primitive await
:
def await[A](p: js.Promise[A]): A = throw new Error("stub")
This can be implemented as a unique JS helper of the form
"await": new WebAssembly.Suspending((x) => x),
When calling any JS function that returns a js.Promise
in its API, we can follow up with a call to this primitive to exploit JSPI and give us the result.
Second option: offer a generic @JSImportSuspending
annotation for @js.native def
s:
@js.native
@JSImportSuspending("readFile", "node:fs")
def readFile(f: String, charset: String): String = js.native
This is closer to the spirit of the JSPI API design, but it means we also need an @JSGlobalSuspending
, a variant with a globalFallback
, etc.
Implementation concerns
On the call path between a promising export and a suspending import call, there cannot be any JavaScript frame. This cannot reasonably be checked statically nor dynamically. However, it will be checked by the engine at run-time, which will trap
if the condition is not upheld. There is currently no way to test ahead of time, and since a trap
is not catchable from Wasm, we cannot recover from that. That is quite annoying, since otherwise in fastLinkJS
our linker guarantees that the code it generates never traps. For example, even a testing framework would have no way of catching this condition; it will crash instead.
Design concerns
This feature is unique to the combination of JavaScript and Wasm. JS alone cannot do it, nor can Wasm alone. Therefore, this feature would be unique to Scala.js-on-Wasm! Given the current experimental status of our Wasm backend, it's not something we can really do today. However, it gives such a new superpower that I believe we should already start considering how we could offer that superpower to our users.