ruby.wasm で await を使う
最近はずっと 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 においてある。今後も変更していくと思うけど参考までに。
Discussion