Slide 1

Slide 1 text

非同期処理を活用しながら Rust製wasmとJSを連携する方法 (wasm-bindgenを使いたくない人向け) 2024-08-24 フロントエンドカンファレンス北海道

Slide 2

Slide 2 text

発表者紹介 uhyo 株式会社カオナビ フロントエンドエキスパート 好きなプログラミング言語は TypeScriptとRust。

Slide 3

Slide 3 text

宣伝 株式会社カオナビはフロントエンドカンファ レンス北海道のゴールドスポンサーです 午前中にスポンサーLTもありました

Slide 4

Slide 4 text

このトークのテーマ: WASM JavaScriptエンジンに処理系が組み込まれており、 フロントエンドからも利用可能な実行可能形式。 多くの言語からコンパイルターゲットとして 利用可能であり、JS以外で書いたコードを フロントエンドで動かす有力な手段である。

Slide 5

Slide 5 text

WASMの進化 WASMは基本機能がすでに標準化されて各処理系 に組み込まれているが、 さらなる進化が提案・検討されている。 WebAssembly proposals https://github.com/WebAssembly/proposals

Slide 6

Slide 6 text

WASMと非同期処理 非同期処理に関する機能はまだ完成していない。 関連するプロポーザル: • Threads • Stack Switching • JS Promise Integration (JSPI)

Slide 7

Slide 7 text

現状のWASMは同期処理である 現状のWASMの実行モデルは単純であり、 呼び出し側(JS側)から見たら同期関数に見える。 つまり、JSとWASMでコールスタックを共有する。

Slide 8

Slide 8 text

コールスタック共有の例 WASM側のコードはRustで示します 呼び出し

Slide 9

Slide 9 text

コールスタック共有の例 JS→WASM→JSのように関数呼び出しをした場合、 このように同じコールスタックに混ざる。 JSの関数もWASMの関数も 同期関数として振る舞っている。 JS JS WASM

Slide 10

Slide 10 text

現状のWASMと非同期処理の問題 JS側で非同期処理が発生した場合、 WASM側が「待つ」方法が現状存在しない。 WASM JS これやって (関数呼び出し) ちょっと待ってね (Promise返し) え? 何これ

Slide 11

Slide 11 text

このトークの内容 JS側で非同期処理を使いつつ、 WASMと連携する方法を2つ解説。 ① JSPIを使う方法 用意されたAPIを使えば簡単にできる ② 自前のイベントループを用意する方法 WASM側でRustのasync/awaitを使える! 「JS側の非同期処理を.awaitする」とかできる

Slide 12

Slide 12 text

補足: wasm-bindgen このトークではwasm-bindgenは扱いません。 Rustに組み込みのWASM対応のみ使用。 wasm-bindgen不使用 wasm-bindgen使用 WASM Rust コンパイル WASM Rust コンパイル JS Promise対応とかもあるらしい

Slide 13

Slide 13 text

JSPI (JS Promise Integration)

Slide 14

Slide 14 text

JSPI JS側の非同期処理が完了するまで、WASM側を 止めておくことができる仕様。 実験的機能としてChromeに実装されている。

Slide 15

Slide 15 text

JSPIの例 WASM側は、JS側から getA, getB, getCを インポートする。 runが実行されると、 3つの関数を実行して 結果を足した値を 返す。

Slide 16

Slide 16 text

JSPIの例 getAとかは実はJS側では非同期関数になっている。 WebAssembly.SuspendingでラップしてWASM側に渡す ことで、WASM側から同期関数に見える。

Slide 17

Slide 17 text

JSPIの例 WASMのエントリーポイントとなる関数を、 WebAssembly.promisingでラップする必要がある。 ラップしたら返り値がPromiseになる。

Slide 18

Slide 18 text

JS JSPI図解 WASM サスペンド可能な世界 JS promisingでラップ した関数を呼出 Suspendingでラップした関数が Promiseを返した場合、 解決するまでWASMの実行が止まる (非同期処理がWASM側からは同期に見える) Promiseが返る (処理が全部 終わったら解決)

Slide 19

Slide 19 text

JSPI図解 promisingを介してWASMを実行することで、 WASMがasync関数みたいになる。 JS側が返したPromiseは自動的にawaitする。 JS WASM JS async await await await

Slide 20

Slide 20 text

WASMの並行実行 promisingを介して実行した場合、 WASMを複数並行して実行することができる。 (JSのasync関数を複数同時に実行するのと同じ) 複数同時に並列実行され ることはない。 JSと同じ実行モデル。 JS WASM JS

Slide 21

Slide 21 text

JSPIの利点・欠点 利点 WASM側に非同期処理の知識は一切不要。 WASM側では従来通りの同期的な処理を書きつつ、 JavaScript側では非同期処理を利用できる。 欠点 WASM側から複数の非同期処理を並行的に開始す ることはできない。(複雑なアプリの実装に不向き)

Slide 22

Slide 22 text

自前のイベントループを 用意する

Slide 23

Slide 23 text

背景 Rust製のCLIツールをWASMにコンパイルして Node.js経由で実行する形でnpmで配布している。 一部の処理をNode.js側に移譲しており、Node.js 側の処理を待つ間もWASM側を実行し続けたい。 (JSPIでは難しいユースケース)

Slide 24

Slide 24 text

WASM側中心のアーキテクチャ WASMがメインのアーキテクチャ(WASIも活用) なので、非同期処理もWASM側で取り扱いたい。 具体的には、Rustのasync/.awaitを使いたい。 JS側の非同期処理をRustのFutureとして扱いたい。 (FutureはJSのPromiseみたいなやつです)

Slide 25

Slide 25 text

Rustの非同期処理の例 JSの実行をNode.jsに任せているところ 非同期処理

Slide 26

Slide 26 text

自前のイベントループ コード実行 非同期処理発生 Node.jsに処理を発注 結果受け取り処理 タスクを 登録 Node.js側で処理完了 JS WASM

Slide 27

Slide 27 text

コード実行 自前のイベントループ(別視点) コード実行 非同期処理 発生 Node.jsに処理を発注 結果受け取り 処理 Node.js側で処理完了 起動 非同期処理 発生 Node.jsに処理を発注 同期実行 同期実行 WASMの同期実行は、 できるタスクが無くなったら returnしてJSに制御を戻す。 非同期処理が完了したら、 JS側からWASMを呼び出して できるようになった タスクを走らせる。 JSのイベントループ (マイクロタスク)と 考え方は同じ。 関数呼び出し return 呼出 return

Slide 28

Slide 28 text

Rustの非同期ランタイム Rustでのasync/.awaitの実務は 非同期ランタイムが担当する。 Tokioなどが知られており、 WASM対応もされている。 https://tokio.rs/

Slide 29

Slide 29 text

Rustの非同期ランタイム WASM含むシングルスレッドの環境では、 非同期ランタイムの実態はイベントループになる。 しかし、今回の要件にはTokioは採用できなかった。

Slide 30

Slide 30 text

Tokioを採用できない理由 Tokio(のようなハイレベルなランタイム)では、 イベントループを回すイベントが 決め打ちされている。 (ネットワークIO、ファイルIO、タイマー) 「Node.js側で非同期処理が終わったとき」 のようなイベントは作れない。

Slide 31

Slide 31 text

ローレベルな非同期ランタイムを使う 今回はasync-executorを採用。 「タスクを積む」「イベントループを1週回す」 というローレベルなAPIを提供しており、 Node.js側で非同期処理が完了したらイベント ループが再開する挙動を実装できる。 https://github.com/smol-rs/async-executor

Slide 32

Slide 32 text

JS側とWASM側の通信 ① WASMからJSにコード実行を依頼 WASM JS execute_node コード文字列とハンドル(i32)を JS側に渡す JS側で非同期的 にコードを実行

Slide 33

Slide 33 text

JS側とWASM側の通信 ② JSからWASMに実行結果を渡す WASM JS execute_node_ret ハンドルと実行結果を渡す そのままWASM側の イベントループ再開 タスクが無くなったら 制御が戻る

Slide 34

Slide 34 text

JS側とWASM側の通信 コード実行 非同期処理発生 Node.jsに処理を発注 結果受け取り処理 タスクを 登録 Node.js側で処理完了 execute_node execute_node_ret

Slide 35

Slide 35 text

JS側のイベントループとの統合 この構成は、JSのイベントループの一部として WASMのイベントループを動かしているという見 方もできる。 コード実行 非同期処理発生 結果受け取り 処理 JSイベント ループ 非同期処理

Slide 36

Slide 36 text

実際のコード (JS側) やっていることは、 渡された文字列を読む →非同期的に 実行する (w.run) →結果が出たら WASM側に送る (execute_node_ret) 単純!

Slide 37

Slide 37 text

WASM側のコード(Rust)もチラ見せ 非同期処理を表す構造体に Futureを自前実装

Slide 38

Slide 38 text

WASM側のコード(Rust)もチラ見せ JS側から非同期処理の 結果を受け取る 結果をメモリに記録 イベントループを回す この処理を待っている Futureを起こす

Slide 39

Slide 39 text

今後の展望 現在、Node.js側に非同期処理として任せている のはJSの実行のみ。 ファイル読みこみなどはWASIで行っている。 (=非同期にはなっていない) ここも非同期化できたら面白そう。 しかし独自実装が増えるので大変。

Slide 40

Slide 40 text

まとめ Rust製のWASMでJS側の非同期処理を待つ、 しかもそれをRustのasync/.awaitで表現する ためには、 ローレベルな非同期ランタイムを用いて自前の イベントループを実装し、 WASMのイベントループをJSのイベントループの 一部として扱えばいい。 ※2024年8月現在の情報です。

Slide 41

Slide 41 text

宣伝 今回お話ししたアイデアが実際に実装されている ソースコードはこちらで見ることができます。 https://github.com/uhyo/nitrogql