最近はずっと ruby.wasm で遊んでます。
2023/5/19 に ruby.wasm 2.0 が出ました。
ruby.wasm 1.0 では await がうまく動かないことがあったけど、2.0 でちゃんと動くようになったんで、記念に前の記事以降にやったこと等をまとめてみた。
await
ruby.wasm で await を使うには2つ問題がある。
- Ruby スクリプトを eval ではなく evalAsync で実行する必要がある。
- スタックサイズが小さくてすぐに SystemStackError エラーが出てしまう。
Ruby スクリプトを eval ではなく evalAsync で実行する必要がある
HTML 内で <script type="text/ruby">
で気軽に Ruby スクリプトを書いたときに await
を使うとエラーになってしまう。(ruby.wasm 1.0 ではエラーにならずに nil が返される)
<script src="https://cdn.jsdelivr.net/npm/ruby-3_2-wasm-wasi@2.0.0/dist/browser.script.iife.js"></script> <script type="text/ruby"> require 'js' def start p JS.global.fetch('hoge.html').await #=> JS::Object#await can be called only from evalAsync (RuntimeError) end start </script>
Ruby スクリプトを実行する部分を eval
から evalAsync
に変えればいいんだけど、面倒くさい。
実は evalAsync
は Fiber 内で eval
してるので、await
したいコードを Fiber 内で動かせばいいだけだったりする。
<script src="https://cdn.jsdelivr.net/npm/ruby-3_2-wasm-wasi@2.0.0/dist/browser.script.iife.js"></script> <script type="text/ruby"> require 'js' def start p JS.global.fetch('hoge.html').await #=> [objcet Response] end Fiber.new{start}.transfer </script>
簡単!
HTLM 要素にイベントハンドラを設定するには addEventListener
を使うんだけど、イベントハンドラ中ではそのままでは await を使えない。
<input id="b" type="button" value="button"></input> <script type="text/ruby"> require 'js' Document = JS.global[:document] b = Document.getElementById('b') b.addEventListener('click') do |e| p JS.global.fetch('hoge.html').await #=> in `await': JS::Object#await can be called only from evalAsync (RuntimeError) end </script>
この場合も Fiber を使えば await を使うことができる。
<input id="b" type="button" value="button"></input> <script type="text/ruby"> require 'js' Document = JS.global[:document] b = Document.getElementById('b') b.addEventListener('click') do |e| Fiber.new do p JS.global.fetch('hoge.html').await #=> [objcet Response] end.transfer end </script>
スタックサイズが小さくてすぐに SystemStackError エラーが出てしまう
Fiber のスタックサイズが小さいので、こんなスクリプトでも SystemStackError が出ちゃう。
<script src="https://cdn.jsdelivr.net/npm/ruby-3_2-wasm-wasi@2.0.0/dist/browser.script.iife.js"></script> <script type="text/ruby"> require 'js' def start pp [1] * 30 #=> Uncaught (in promise) Error: SystemStackError: stack level too deep end Fiber.new{start}.transfer </script>
これはもうどうしようもないので、ruby.wasm をビルドし直す必要がある。めんどくさい。
ここの new WASI({});
を
const wasi = new WASI({ env: { "RUBY_FIBER_MACHINE_STACK_SIZE": "1048576" } });
のように変更してビルドする。
でもめんどくさいので、簡単にやるなら、
curl https://cdn.jsdelivr.net/npm/ruby-3_2-wasm-wasi@2.0.0/dist/browser.script.iife.js | sed -e 's/const wasi = new s.*/const wasi = new s({env:{"RUBY_FIBER_MACHINE_STACK_SIZE":"1048576"}});/' > browser.script.iife.js
のようにして、ビルド済みのファイルの中を強制的に置換したものを使うという手もある。
<script src="browser.script.iife.js"></script> <script type="text/ruby"> require 'js' def start pp [1] * 30 end Fiber.new{start}.transfer </script>
とここまで書いて、ruby.wasm の main だとデフォルトで 16777216
に変更されていることに気がついた。次のバージョンでは何もしなくても使えるようになるかも。
デイリービルドのやつでよければ、https://cdn.jsdelivr.net/npm/ruby-3_2-wasm-wasi@2.0.0-2023-05-21-a/dist/browser.script.iife.js を使うだけでいけた。
Ruby 風にする
その他、いろいろ Ruby っぽく書けるようにした。
関数名をスネークケースで呼び出し
ruby.wasm の JS ライブラリは薄いラッパーなので、getElementById()
みたいに JavaScript の関数名がそのまま使われる。
あまり Ruby ぽくないので、get_element_by_id()
みたいなスネークケースでも呼べるようにした。
module JSrb def method_missing(sym, *args, &block) sym = sym.to_s.gsub(/_([a-z])/){$1.upcase}.intern super(sym, *args, &block) end end class JS::Object prepend JSrb end
みたいな感じ。
プロパティを .prop_name
形式でも参照
JS::Object
のプロパティを参照するには [:propName]
のようにするけど、JS::Object.prop_name
でも参照したい。
prop_name
を propName
に変換して [:propName]
を呼び出せばよさそう。
module JSrb def method_missing(sym, *args, &block) sym = sym.to_s.gsub(/_([a-z])/){$1.upcase}.intern v = self.method(:[]).super_method.call(sym.intern) v = self.call(sym, *args, &block) if v.typeof == 'function' v end end
こんな感じで。プロパティが関数の場合はそれを呼び出してる。
プロパティを設定するには、sym
が =
で終わってる場合に同じような感じで []=
を呼べばいい。
プリミティブ型を Ruby のオブジェクトに変換
JavaScript の値は Ruby からは JS::Object
として見えるので、そのままだと使いにくい。
値の型に応じて JS::Object#to_i
や JS::Object#to_s
みたいにして使うことになる。
これは、ベタだけど JS::Object#typeof
を見て変換する感じで。
case v.typeof when 'number' v.to_s =~ /\./ ? v.to_f : v.to_i when 'bigint' v.to_i when 'string' v.to_s when 'boolean' v.to_s == 'true' else if v.to_s =~ /\A\[object .*(List|Collection)\]\z/ v.length.times.map{|i| v[i]} elsif v == JS::Null || v == JS::Undefined nil else v end end
〜List
とか 〜Collection
という名前のオブジェクトは Array
にしたり、null
や undefined
は nil
にしたり。
この辺はかなりテキトーなので、うまく動かないこともあるかもしれない。
まとめ
これで、次のように書いてたスクリプトは、
children = JS.global[:document].getElementById('hoge')[:children] children[:length].to_i.times do |i| p children[i][:tagName] end
次のように書けるようになる。
JS.global.document.get_element_by_id('hoge').children.each do |c| p c.tag_name end
かなり Ruby っぽくなった。
Ruby ぽくするやつの全体は https://mysql-params.tmtms.net/lib/jsrb.rb においてある。今後も変更していくと思うけど参考までに。