【WASM】例外を扱う

2023-02-10

WASMは通常の制御フローしかなくて大域ジャンプはできないものかと思い込んでいたが、例外も使えるということを知ったのでどんなもんか調べた。

概要

ドキュメントはWebAssembly/exception-handlingに書かれている:

  • throw で例外を起こす、「タグ」で識別する
  • trycatchはWASMバイトコードのブロック
  • rethrow は何段投げるかを指定する(ラベルも可)
  • delegate で上に任せることもできる

ドキュメントだけだとよくわからなかったので、WATソースを書いて動かしてみた:

;; try.wat
(module
(tag $NOT_ENOUGH_MONEY_EXCEPTION (export "NOT_ENOUGH_MONEY_EXCEPTION") (param i32))

(func $main (export "main") (result i32)
try $l1 (result i32)
i32.const 200
i32.const 300
call $purchase
catch $NOT_ENOUGH_MONEY_EXCEPTION
drop ;; i32
i32.const -1
catch_all
i32.const -2
end
)

(func $purchase (param $cache i32) (param $price i32) (result i32)
local.get $cache
local.get $price
i32.lt_s
if
local.get $price
local.get $cache
i32.sub
throw $NOT_ENOUGH_MONEY_EXCEPTION
end

local.get $cache
local.get $price
i32.sub
)
)

上記を wat2wasm でコンパイルするが、現状ではオプション --enable-exceptions を指定する必要があった(バージョン1.0.32)。

  • タグセクション(13)が追加で出力される
    • --verbose 指定でどんなバイナリが出力されるかがわかりやすい

そしてrunwasmを使って実行: node runwasm.js try.wasm main

  • node.js 18より前だと --experimental-wasm-eh オプションを指定する必要がある
  • 例外なので当然ながら、try内から呼び出した関数内でthrowすると関数を飛び越えてcatchできる
    • タグに対応する型でパラメータを送れて、キャッチ側でそれを利用できる
  • WASM内でキャッチされなかった例外はNode.js側でもキャッチできる
    • 例外はJavaScriptの WebAssembly.Exception クラス: if (e instanceof WebAssembly.Exception) ...
    • タグもexportできるので、それを使って判定できる:if (e.is(instance.exports.NOT_ENOUGH_MONEY_EXCEPTION) ...

ブラウザで動くか?

wabtやNode.js 16だとコマンドラインオプションが必要だったが、Google ChromeやSafariでは問題なく動いた: JavaScript built-in: WebAssembly: Exception: is | Can I use… Support tables for HTML5, CSS3, etc

その他

  • 実際の例外処理:WASMでは単純な数値型以外は線形メモリ上に置く必要があってその位置をグローバル変数で管理することになるが、例外で大域ジャンプが行われると値がずれてしまう可能性があるので調整する仕組みを入れる必要がある
  • C言語からのコンパイルでsetjmpが動くようにしてやろうと考えたが、trycatchにするには代入先の変数名を特定して構文木を書き換える必要があるので、ちょっと手間がかかりそう 対応した