Scala.js + Javy で Scala を WebAssembly 上で動かす
Scala Advent Calendar 2023 1日目の記事です。
最近はScalaのWebAssembly対応やりたいな〜と思ってWebAssemblyの勉強をしています。
(前置き) Scala の WebAssembly 対応 (2023)
ScalaからWebAssemblyを生成する方法はいくつかあるのですが、Kotlin/WASM のようにScalaコンパイラがWebAssemblyを直接生成ことは今のところできません。
いくつかの方法というのは例えば
TeaVM
TeaVMはJVMバイトコードをJSやWASMにAOTコンパイルしてくれるツールです。sbtからも使える🎉
WebAssembly 対応は experimental とのことで、以前試したときは確かにいろいろ動かなかった気がする
Scala Native
- Scala Native は LLVM をバックエンドにしたコンパイラ
- LLVM 吐き出せるなら Emscripten や WASI-SDK で WebAssembly 吐けるやん
- WASI-SDK で exception handling 今のところなし RFC: Add Wasm exception support by whitequark · Pull Request #198 · WebAssembly/wasi-sdk
- LLVM から Emscripten や WASI-SDK 使って wasm gc primitive を生成する方法今のところ無さそう?
新しいバックエンドが必要
以上の方法では実用的なWASMを生成することができません。WASMをちゃんとサポートするなら、ScalaコンパイラがWASMを吐き出す新しいバックエンドを作るのが良さそうというのがScalaコンパイラ開発チームの今のところの(うちうちの)見解となっています。
とはいえ仮にScalaがwasm gcやWASIバックエンドを実装したとしても、wasmtime・wasmedge・wasmer(?)はまだgc[1]もexception handling[2]も実装されておらず、browser-embedding はまだしも、WASI対応はまだまだ遠い話になりそう...[3]
もしくは AssemblyScript なんかみたいに自前でGCを実装するか、だけどなんかwasmgcが上記のruntimeで使えるようになったらいらなくなるだろうしな〜どうしよう
Javy
そうしたらJavyというJSをWASM上で実行するツールをShopifyが開発しているという記事を見つけました。
詳しくは上の記事を見ると良いのですが、JavyはJavaScriptをWebAssembly(厳密には wasm32-wasi
)上で動かすためのツールチェーンです。
Javy は、JSをWASMにコンパイルするわけではなく、WASMにコンパイルされたCで書かれた小さくて埋め込み可能なJavascriptエンジンである QuickJS
を使用して、WASM上で(QuickJS
バイトコードにコンパイルされた)JSを埋め込まれたQuickJSエンジンを使った実行する形です。
そのため、例外処理やPromiseのような高レベルの機能も問題なく動作するし、QuickJS
のGCのおかげでメモリを食いつぶすこともない。
Scala.js + Javy
ところで、ScalaにはScala.jsという恐ろしく完成度の高いScala->JSコンパイラバックエンドがありまして...
じゃあ Scala->(scala.js)->JS->(Javy)->WASI
で Scalaで書いたコードがだいたいWASM上で動かせるんじゃない...? ということでやってみました。
Exception Handling
例外が動くか見るために以下のような簡単なコードをコンパイルしてみよう
// Hello.scala
import scala.scalajs.js
import java.lang.Throwable
object Hello:
def main(args: Array[String]): Unit =
val console = js.Dynamic.global.console
try
throw new Error("test")
catch
case e: Throwable => console.log(e.getMessage)
これを scala-cliを使ってJSにコンパイルし、JavyでWASMにコンパイルし、wasmtimeで実行してみる
$ scala-cli package --js Hello.scala -o build/hello.js --force
$ javy compile build/hello.js -o destination/hello.wasm
$ wasmtime destination/hello.wasm
java.lang.Error: test
できました🎉
Future / Promise
ScalaのFuture
はJS上ではevent loopを使って実装されています。(参考: JavaScriptでScalaのFutureを表現する)
しかし、Javyは(現時点では)デフォルトではQuickJSのevent loopを有効化していません。
we haven’t enabled the event loop in the QuickJS instance that Javy uses. That means that async/await, Promises, and functions like setTimeout are syntactically available but never trigger their callbacks. We want to enable this functionality in Javy, but have to clear up a couple of open questions, like if and how to integrate the event loop with the host system or how to implement setTimeout() from inside a WASI environment.
https://shopify.engineering/javascript-in-webassembly-for-shopify-functions
なので、以下のようなコードを書いて
// Promise.scala
import scala.concurrent._
import scala.util.Success
import concurrent.ExecutionContext.Implicits.global
object Promise:
def fetchData(): Future[String] = Future { "some data!" }
def main(args: Array[String]): Unit =
val f = fetchData()
f.onComplete:
case Success(data) => println(data)
WASMにビルドして普通にJavyで実行しようとすると失敗してしまう。
$ scala-cli package --js Promise.scala -o build/promise.js --force
$ javy compile build/promise.js -o destination/promise.wasm
$ wasmtime destination/promise.wasm
Error while running JS: Adding tasks to the event queue is not supported
Error: failed to run main module `destination/promise.wasm`
Caused by:
0: failed to invoke command default
1: error while executing at wasm backtrace:
0: 0x5cfa1 - <unknown>!<wasm function 104>
1: 0x6f59c - <unknown>!<wasm function 165>
2: 0xb6630 - <unknown>!<wasm function 1005>
2: wasm trap: wasm `unreachable` instruction executed
experimental_event_loop
どうやらEvent Loopは experimental_event_loop
フラグで有効化できるようなので、試してみる。(Javyをそのフラグ付きでビルドする)
$ cargo build --features experimental_event_loop -p javy-core --target=wasm32-wasi -r
$ cargo install --path crates/cli
$ javy compile build/promise.js -o destination/promise.wasm
$ wasmtime destination/promise.wasm
some data!
動きました🎉 (どんなリスクがあるのか分かってないけど!)
他のプラットフォームでの活用
例えば
- WASMベースのmicroserviceを実装するためのフレームワークspinの、spin-js-sdkもJavyを使って実装されています。
- またWASMによるuniversal plugin systemを実装するExtismの、js plugin development kitもJavyを使って実装されています。
Scala.jsではScalablyTypedというツールを使うことで簡単にTSライブラリのScalaバインディングが生成可能なので、同じ要領でScalaでWASMベースのmicroserviceやpluginを実装することができます。
Scala on Fermyon Cloud
実際にspin-js-sdk、scala.js、ScalablyTypedを使って、Scalaで書いたコードをWASM上で動くマイクロサービスにしてみました。
ここでは詳しく書かず、また別の記事で書くことにします。
Cons
これでScalaをWASM上で動かすことができました!
しかし、デメリットもあります。というのも残念ながら、KotlinやDart、そしてもちろんC++やRustによるWASMと比べると、実行速度は劣るのではないか(ちゃんと計測してない)。その理由は
- (1) 一つは、WASMモジュールに組み込まれたJSエンジン上でコンパイルしたJSコードを実行するだけだからです。WASMを直接吐き出してそれを実行するほうが早そうに思える
- QuickJSをサイドモジュールとして動的リンクすることで、WASM moduleのサイズを小さくすることもできますが、static linkするとそのぶんmodule sizeも大きくなる
- (2) もう1つの理由は、QuickJSは小さく組み込み可能である代わりに、V8やSpiderMonkeyのような他の大規模JSエンジンよりも遅いからです。
- 実際、QuickJSの公式ベンチマークによると、JITが有効なV8はQuickJSの最大30倍速くなるそうです。https://bellard.org/quickjs/bench.html。
Pros
しかし、良い部分もあります。WASM化はすべて実行パフォーマンスのためと考えると、Javy+Scala.jsでScalaをWASM上で動かすメリットはあまり感じられないかもしれません。
しかし、下記ブログで述べられているように、WASMの利点は実行速度だけでなく、起動パフォーマンスが高いこと、セキュリティ、ポータビリティなどもあります。
特に、Scala.jsとJavyを使ってScalaをコンパイルするメリットは、wasm32-unkown-unknown
ではなく、GCや例外機構などの高水準な機能を備えたwasm32-wasi
にコンパイルできることだと思います。これにより、ScalaをWASMベースのプラットフォームで利用できるようになる
- Shopify Functions・Extism・dprintのなどのためのをScalaで記述できる
- また、Scalaコードをfermyon cloud や wasm worker server などの wasm ベースのマイクロサービスプラットフォームにデプロイできる
- edge computingプラットフォーム上でScalaを実行する(ただし、エッジに置けるようにバイナリサイズを縮小する必要があるが...)
- NearなどのWebAssemblyを実行するWeb3プラットフォームでScalaを利用できる
ScalaとWASMの今後
(個人の意見、今開発チームとお話している最中です)
Scala.js + Javy でScalaをWASM上で実行できるようになった。[4]
じゃあ、もうScalaのWASMサポートはこれでおしまい?かというと当然そんなことはない。Kotlin/WASMがそうしたように自前のWASMバックエンドを実装することで生成されるWASMバイナリサイズを大きく削減することができるだろう。またScalaのwit-bindgenも必要になってくるだろう。
WASMバックエンドは、WASM GCに乗っかるべきだろうか?それともAssemblyScriptなどのように自分たちでlinear memoryに対するGC機構を実装するべきだろうか? - 個人的には WASM GCに乗っかっていくと良さそうな気がしている。
最近は多くのプログラミング言語(KotlinやOCamlやbinaryen IR)がWASM GC primitiveを実装し、またV8などもWASM GCをサポートしている。長い目で見れば、wasmtimeやwasmedgeのようなランタイムはいずれwasm gcをサポートするでしょう...(知らんけど)[5]
それらのプラットフォームがwasm gcを実装してくれれば、我々がGCを自前で実装する必要もなくなるだろう。
当面の間は今回紹介した方法でWASMにコンパイルしつつ、(WASMサポートを少しでも簡単にするためにも)自分たちでGCを実装するのではなくWASM GCにコンパイルすることに集中するべきかもしれない
-
https://github.com/bytecodealliance/rfcs/pull/31 https://github.com/WasmEdge/WasmEdge/issues/1122 ↩︎
-
https://github.com/bytecodealliance/wasmtime/issues/3427 https://github.com/wasmerio/wasmer/issues/3100 LLVMベース言語仲間のCrystalさんも困ってます https://github.com/crystal-lang/crystal/issues/13130 ↩︎
-
(ところでGC言語で WASM for browser-embedding って、BlazerやFlutter on the Web みたいなその言語だけでWebアプリケーション作るぜ!ってplatform以外だと嬉しいことあるのかな...?) ↩︎
-
ビルドターゲットはwasm32-wasiだが、ブラウザで実行したければ(そんなことある?)browser_wasi_shimを使えばよいだろう。 ↩︎
-
wasmtime 上で WASM GC を実装するための RFC はすでにマージされています https://github.com/bytecodealliance/rfcs/pull/31 またwasmedgeもwasm gcの実装に励んでいるらしい https://github.com/WasmEdge/WasmEdge/issues/1122 ↩︎
Discussion