2 ポイント 投稿者 GN⁺ 2024-11-11 | 1件のコメント | WhatsAppで共有
  • Jawsm は Rust で書かれた JavaScript→WebAssembly コンパイラで、インタープリタなしで実行可能なスタンドアロンの WASM バイナリを作る実験的ツール
  • porffor と似た standalone WASM を作るが実装方式は異なり、最新の WASM GC、例外処理、tail call 最適化提案の命令を活用して JavaScript 構文を WASM 命令へ変換する
  • 現在 test262 テストスイートの約 25% を通過しており、実用性の確認に重要と見なされた scopes/closures、try/catch、async/await、generators は実装済み
  • まだ本番投入可能な状態ではなく、多くの言語機能と組み込み型が欠けているか不完全で、RegExp、ほとんどの builtins、BigInt arithmetic も未実装
  • 生成されるバイナリは最新の WASM 提案への依存のためランタイム間の移植性が低く、現在は V8 ベースの Chromium または Node 上で WASIp2 polyfill とともに実行する方式が使われている

Jawsm の目標と立ち位置

  • Jawsm は “awesome” のように発音する JavaScript to WebAssembly コンパイラ
  • Rust で書かれており、JavaScript コードをインタープリタなしで実行できる standalone WASM binary にすることを目標としている
  • porffor と似た成果物を作るが、実装アプローチは異なる
  • 現在は 実験的ツール であり、本番利用の準備はできていない
    • 多くの JavaScript 言語機能が欠けているか不完全
    • 多くの組み込み型とメソッドも欠けているか不完全
  • 長期目標は JavaScript 言語機能を 100% サポート すること

なぜ Jawsm を作ったのか

  • プロジェクトは WebAssembly シナリオを実行するストレステストツール Crows の開発中に始まった
  • Crows は現在 Rust から WASM にコンパイルしたコードのみをサポートしている
  • 小さなテストは interpreted language で書いたほうが簡単なことが多いが、WASM 上で scripting language を動かす現在の方法は理想的ではない
    • インタープリタを含めるとバイナリサイズは最低でも数 MB になり、メモリ使用量も大きくなる
    • あるいは TinyGo や AssemblyScript のように対象言語の変種を使う必要がある
  • 現代の WASM 提案を活用すれば、コンパイル済みインタープリタなしでも JavaScript 機能を 100% 実装できるという目標を持っている
  • WASM ランタイム自体がすでに インタープリタ であるという前提に立っている

現在動作する機能

  • Jawsm は現在 test262 テストスイートの約 25% を通過している
  • プロジェクトの実用性確認のための中核機能 4 つはすべて実装済み
    • scopes/closures
    • try/catch
    • async/await
    • generators
  • そのほか動作する機能は以下のとおり
    • varletconst の宣言と代入
    • do..whilewhileforfor..infor..of ループ
    • switch
    • breakcontinue の限定的サポート
    • 文字列リテラルと文字列リテラルの加算
    • 数値と基本演算子 +-*/
    • boolean と基本 boolean 演算子
    • 配列と Array 関連関数の大半
    • object literals
    • new キーワード
    • asyncawait
    • Promise API の限定的サポート
    • generator functions
    • try/catch
    • ごく基本的な BigInt サポート

まだ不足している機能

  • 現在不足している主な項目は以下のとおり
    • ほとんどの builtins
    • 既存 builtins のほとんどのメソッド
    • RegExp expressions
    • BigInt arithmetic
  • 次の段階の計画は以下の機能実装に重点を置いている
    • 基本的な regexp サポート
      • RegExp literals
      • RegExp オブジェクトのごく基本的な関数
    • BigInt literals と BigInt の基本サポート
    • equality チェックや複数の演算子使用時のより良い automatic casting
    • arrays、strings など基本 builtins のさらなる関数

実行環境と制約

  • Jawsm は比較的新しい WASM 提案をいくつか使っているため、生成されたバイナリはまだランタイム間の移植性が高くない
  • 目標実装は WASIp2 を念頭に置いている
  • Wasmtime は components と WASIp2 を実行できるランタイムだが、Jawsm が使う WASM GC の一部や exception handling のような項目には対応していない
  • ランタイムが標準化された提案に追いつくまで、開発を容易にするため V8 を使っている
    • Chromium または Node を通じて V8 を利用する
    • 必要な WASIp2 機能は JavaScript polyfill で補う
  • リポジトリには Jawsm が生成したバイナリを実行する run.js スクリプトがある
  • 最終的には WASM GC、exception handling、WASIp2 API を実装したあらゆるランタイムで実行可能になることを目指している
    • あるいは WASIp2 polyfill を使う方法も含まれる

使い方

  • コントリビュート目的でなければ、現時点では使用は推奨されない
  • リポジトリを clone した後、execute.sh を次のように使える
./execute.sh --cargo-run path/to/script.js
  • このコマンドは WAT ファイルを生成し、バイナリにコンパイルしてから Node.js で実行する
  • 必要なツールは以下のとおり
    • Rust の cargo
    • 比較的新しいバージョンの wasm-tools
    • Node.js v23.0.0 以上
  • --cargo-run オプションを渡すと、まず cargo run でプロジェクトをコンパイルしてから実行する
  • --cargo-run なしで実行すると release build を実行しようとするため、先に cargo build --release を実行する必要がある

内部動作の仕組み

  • Jawsm は JavaScript syntax を WASM instructions に変換する
  • 変換処理では次の WASM 提案の命令を活用する
    • WASM GC

      • exception handling
      • tail call optimizations
      • Rust コードがスクリプトを変換し、JavaScript semantics を WASM に移すための型と関数群が一緒に使われる
      • 大半の WASM instruction は tarnik を使って生成される
      • tarnik は Rust-like syntax をベースに WASM instructions を生成する Rust macro

scope と closure の処理例

  • WASM は function references、structs、arrays をサポートするが、JavaScript の scope semantics は直接提供しない
  • Jawsm は JavaScript の scope 動作を模倣するために追加の WASM コードを生成する
  • 例となる JavaScript コードは次のとおり
let a = "foo";

function bar() {
  console.log(a);
}

bar();
  • JavaScript では関数定義は自分が定義された scope を継承するため、bar() は変数 a にアクセスできなければならない
  • 変換の流れはおおむね以下のとおり
    • parent を持たない global scope を作成する
    • 現在の scope に変数 a"foo" で宣言する
    • bar 関数オブジェクトを作るとき、関数が定義された scope への参照も一緒に保存する
    • 関数内部の実行時には新しい scope を作るが、parentScope 参照は維持する
    • retrieve(scope, "a") は現在の scope とすべての parent scope から a を検索する
    • 現在の scope から bar を取得して呼び出す

ライセンス

  • コードは Apache 2.0 license で配布されている

1件のコメント

 
GN⁺ 2024-11-11
Hacker Newsのコメント
  • WASM GC提案を本当に巧みに活用している
    これまでのJS→WASMコンパイラは、実質的にJSエンジン全体を載せる方式だったが、JSの構造をWASMのプリミティブ機能に直接マッピングしようとするのは初めて見た

    • Porffor https://porffor.dev/Static Hermes https://hermesengine.dev/ もコンパイル方式を取っているようだ
      Jawsと比較すると面白そうだ
    • その通り。ただ正直に言うと、WASM上に完全なインタプリタを書くほど賢くなかったので生まれた、巧妙な活用に近い
  • 以前、ほとんどTypeScriptに近い言語を 組み込みARMコンパイラ として作ったことがある
    AssemblyScriptよりずっとTypeScriptに近く、そのとき使った手法の一部が役に立つかもしれない
    https://www.microsoft.com/en-us/research/uploads/prod/2019/09/static-typescript-draft2.pdf

  • 「Rustを使うのは本当に好きだが、広く人気のある言語ではないことも分かっている」というのは正しいのか?
    Rust はものすごく誇大宣伝されていて、最近はどこでも使われているように見える

    • まだ 暗号資産関連ではないRustの求人 を見つけられていない
      私の国エストニアでは、人々が使う地元の求人掲示板ベースで地域の求人が0件なので、実際に重要な意味ではまったく人気があるとは言い難い
    • ソーシャルで誇大宣伝され目立っているからといって、必ずしも広く使われているという意味ではない
      「広く」をどう定義するかによるが、StackOverflowやPyPLのような指標、GitHub統計を見ると、Rustの使用量は JavaScriptやPythonの5〜10%程度 に見える
  • 「最終的にはJavaScript仕様の100%をカバーできるとかなり確信している」なら、test262_runner.rbの結果 はあるのか?
    Porffor作者の発表でtest262を知り、READMEに https://github.com/CanadaHonk/porffor?tab=readme-ov-file#test262 のような進捗表示があるとよいと思う
    良いプロジェクトだ

    • 現時点ではテストの約 12% を通過しているが、実装しやすい部分がまだ多く残っている
      特にプロジェクトを始めてまだ2週間しか経っていないことを考えると、なおさらそうで、もちろん100%まで線形に進めるという意味ではない
      組み込み型や関数のロングテールが残っているが、「難しい部分」から実装してきたため、簡単な部分の一部はまだ手を付けていない状態だ
      たとえばtest262ハーネスを回すのに必要な条件分岐とwhileループ程度しか構文を実装しておらず、for、for in、for of、do while、switch のような条件構文はまだ実装していない
      これらは既存のif/elseやwhile実装とほぼ同じように追加できる

      await とジェネレータの実装を終えれば、最後に残る難しい意味論的概念が解決するので、その後はこうした簡単な部分を実装する予定だ
      カバレッジがどこまで上がるかは言いにくいが、たとえば今は object["foo"] 構文が実装されていないため 1200件のテスト が失敗している
      object.foo は動作するが、object["foo"] はまだ動かない状態だ
      その1200件が自動的にすべて通るという意味ではないが、このような比較的単純な構文の欠落が原因で、数百件のテストが失敗していることはよくある

      Porfforのようなクールなグラフもぜひ入れたい

  • プロジェクトのREADME.mdを読んでも、まだよく分からないのだが、想定している使い方は何か?
    出力された WASMコード がどのランタイムとどのように相互作用するのか気になる
    ブラウザと他のWASMランタイムの両方に互換性のあるツールなのか、それともプロジェクトに結び付いたランタイムでしか動かないのかも気になる

    関連して、JavaScriptコードの中でWeb APIや特定環境でのみ定義されたグローバル識別子、たとえば最新ブラウザやNode.jsのグローバル識別子に遭遇したらどう反応するのか?
    そうした環境を対象にしていないなら、I/Oはどうすればよいのか?

    • 良い質問で、READMEにももっと詳しく追記するつもりだ
      このプロジェクトは主に サーバーでのWebAssembly利用 を対象としている
      JavaScriptの中でWebAssembly上にJavaScriptを載せて動かすことにはあまり意味がないと思うが、時間が経てばフロントエンドのプラグインサンドボックス化には役立つかもしれない

      ブラウザで動かすにせよ、WasmTimeやWasmEdgeのようなバックエンドランタイムで動かすにせよ、現時点ではWebAssembly内でJavaScriptを実行するのは理想的ではない
      V8やSpiderMonkeyのようなJSエンジンをWASMにコンパイルしてその上でスクリプトを動かすか、AssemblyScriptのような「ほぼJavaScript」の言語で妥協するしかない
      これがサーバーワークロード実行の制約要因になっている
      たとえばFastlyはWASMワーカーにSpiderMonkeyを使っているが、hello worldだけでもインスタンスごとに 5〜10MBのメモリ を使う
      一方Shopifyはショップのサーバー側カスタマイズにWASMを使い、WASMバイナリを 250KB以下 に制限しているが、このサイズではどんなインタプリタも入れにくい
      そのため「推奨」言語がAssemblyScriptになっており、その理由はここで説明されている: https://shopify.engineering/shopify-webassembly

      こうした状況は、歴史的にWASMが非常に単純なランタイムだったためだ
      Cコードを機械語にコンパイルするようにWASMへコンパイルするのは比較的簡単だったが、WebAssembly自体が一種のインタプリタであるにもかかわらず、その上でさらに高水準言語を解釈するのは容易ではなかった

      いまや ガベージコレクション対応 や例外処理対応のような新しい提案が標準化されつつあり、WebAssemblyは構造体、配列、関数参照のような機能を備えた、はるかに強力なインタプリタになりつつある

Jawsはこの点を活用してJSコードをWASMコードに変換し、SpiderMonkeyのようなJSエンジンなしでWASMに結果コードを解釈させる。
実際には、Jawsが生成するバイナリはおそらく50KB未満にでき、SpiderMonkeyをWASMにコンパイルしてその上でスクリプトを実行する方式の10MBと対照的。
メモリ使用量も大幅に低くなるはず。
これはFastlyのような企業にとって、メモリ使用量とサーバーコストを桁違いに削減できることを意味し、Shopifyのような企業にとっては、既存のJavaScriptコード、たとえばNPMパッケージやJavaScriptエコシステムをバックエンドプラグイン作成者に活用させられることを意味する。

プロジェクトが使用するランタイムはWebAssemblyのみ
生成されたコードは主に、このファイルの約3,000行のWATコード https://github.com/drogus/jaws/blob/main/src/wat/template.wat と、ユーザーのJSコードが変換された部分で構成される。
たとえば "console.log('foo')" のような非常に単純なプログラムでは、全「生成」部分はこれだけ: https://gist.github.com/drogus/1c49c25ed0b14804b2f27e10d2a79928#file-sample-wat-L2690-L2706
これはおおむね引数を new_static_string で用意してから console.log を呼び出している。
現時点ではホスト側に多少のグルーコードが必要だが、最終的にはWASIp2、WASM GC、例外処理提案をサポートするあらゆるランタイムでこうしたバイナリを実行できるようになるはず。

Web APIや環境ごとのグローバル識別子のサポートはまだ実装されていないが、仕組みについては説明できる。
Node.js APIはWASIを通じてサポートする予定。
WASIはWASMプログラムと外部世界が通信するための標準。
たとえばHTTPリクエストの送信、STDOUTへの書き込み、ファイルの読み書きなどに使える標準関数群を定義している。
そのため、fetchfs のようなAPIに到達した場合、WASI preview2をサポートするランタイムでは動作するはず。
ブラウザでもポリフィルでサポートできるが、この場合I/Oサポートはよりカスタム寄りになる。
WASMプログラムにファイルの読み書きを許可するなら、たとえばlocalStorageに保存したり、WASMにコンパイルされたSQLiteデータベースを使ったり、さらにはS3のような場所へ送信したりする仕組みを提供する必要がある。

  • 「ブラウザランタイムなしでJSを実行」が近づいている感じ。
    Porffor、Jaws、あるいは別のプロジェクトのどれかが最終的には成功しそう。

  • このアプローチは本当に気に入った。
    バイナリを直接生成しようとするより、WASMを直接ターゲットにしてビルドすれば、WASM GCやWASI 0.3に含まれる見込みの非同期サポートに依存できる。

  • 文字列エンコーディングの違いや関連ユーティリティはどう処理するのか?
    ぼんやり理解している範囲では、WASMはUTF-8をサポートし、JSは壊れたUTF-16すら扱える。

    • CPU命令セットのように、WASM抽象マシンには文字列やエンコーディングの概念はない。
      線形メモリ内のバイト列でしかなく、必要なエンコーディングは自分で実装すればよい。

      WASMはファイル形式における名前のエンコーディングとしてUTF-8を指定しているが、これはランタイム仮想マシンとは関係ない。

  • それをコンパイラと呼ぶ人もいる。
    ともあれ、よくできている。

    • タイトルを書くとき、表現が少し変だということにまったく気づかなかった。
  • これは同じコードをそのままJSで実行するより速いのか、それとも他言語との相互運用のためのものか?

    • 現段階では何とも言いにくいが、JITが有効なSpiderMonkeyやV8より速くなる可能性はかなり低いと思う。
      現代のJavaScriptコンパイラは、JITによって頻繁に実行される経路をかなりうまく最適化している。
      このプロジェクトの用途は、JavaScriptをWebAssemblyサンドボックス環境で実行できるようにすること。
      たとえばShopifyはWebAssemblyでバックエンドコードを拡張できるようにしているが、バイナリサイズを250KBに制限している。
      そのサイズでは現状JavaScriptを使うのは難しく、QuickJSのような単純なインタプリタでさえWASMにコンパイルすると数MBになるため。