🗿

はじめての ruby.wasm

2023/11/23に公開

https://github.com/ruby/ruby.wasm

Hello, world!

<script src="https://cdn.jsdelivr.net/npm/ruby-3_2-wasm-wasi@2.1.0/dist/browser.script.iife.js"></script>
<script type="text/ruby">
  puts "Hello, world!"
</script>

puts で JavaScript コンソールに出力された。

WASI とは? (3行で)

  • WebAssembly System Interface の略
  • OS みたいなもの
  • W3C が決めている仕様

wasi-vfs をインストールするには?

~/.Brewfile
tap "kateinoigakukun/wasi-vfs", "https://github.com/kateinoigakukun/wasi-vfs.git"
brew "kateinoigakukun/wasi-vfs/wasi-vfs"

としてから

brew bundle --global
$ which wasi-vfs
/opt/homebrew/bin/wasi-vfs
$ wasi-vfs -V
wasi-vfs-cli 0.4.0

https://github.com/kateinoigakukun/wasi-vfs

ドキュメントを読む感じ、wasi-vfs は Ruby 専用というわけではないらしい。

こちらが詳しかった。

https://zenn.dev/0kate/articles/434d7de29cb6af

wasmtime とは? (3行で)

  • WebAssembly 用のランタイム
  • WASI の仕様に沿って実装されたもの
  • Bytecode Aliance (非営利団体) が作っている

https://wasmtime.dev/

wasmtime をインストールするには?

~/.Brewfile
brew "wasmtime"

としてから

brew bundle --global
$ which wasmtime
/opt/homebrew/bin/wasmtime
$ wasmtime -V
wasmtime-cli 14.0.4

いったん整理

  • WASM → CPUの仕様。世界で一つ。
  • WASI → OSの仕様。世界で一つ。
  • wasmtime → WASM と WASI の実装の一つ。他に wasmer もある。

wasmtime で実行する

https://github.com/ruby/ruby.wasm#quick-example-how-to-package-your-ruby-application-as-a-wasi-application

の手順通りにやってみる。

WASM にビルドされている Ruby と関連するものがいろいろ入ってるやつを入れる。

curl -LO https://github.com/ruby/ruby.wasm/releases/latest/download/ruby-3_2-wasm32-unknown-wasi-full.tar.gz
tar xfz ruby-3_2-wasm32-unknown-wasi-full.tar.gz

Ruby 本体をパックしないように抽出する (?)

mv 3_2-wasm32-unknown-wasi-full/usr/local/bin/ruby ruby.wasm

ここで試しに実行してみると本体だけで動いた。

$ wasmtime ruby.wasm --version
ruby 3.2.0 (2022-12-25 revision a528908271) [wasm32-wasi]

アプリのコードを入れる。

mkdir src
echo "puts 'Hello'" > src/my_app.rb

/usr の下のディレクトリ全体とアプリのディレクトリをパックする。

wasi-vfs pack ruby.wasm --mapdir /src::./src --mapdir /usr::./3_2-wasm32-unknown-wasi-full/usr -o my-ruby-app.wasm

パックされたスクリプトを実行する。

wasmtime my-ruby-app.wasm -- /src/my_app.rb

と、「この CLI 呼び出しは、将来別の方法で解析される予定です」という旨の警告がでるので WASMTIME_NEW_CLI=1 をつけると静かになる。

$ WASMTIME_NEW_CLI=1 wasmtime my-ruby-app.wasm -- /src/my_app.rb
Hello

というか、昔は -- が必要だったけど、今はつけなくてよくなったのかもしれない。たんに、

$ wasmtime my-ruby-app.wasm /src/my_app.rb
Hello

としたら警告はでなくなった。

全体的に何が起きているのかいまいちわかっていないが ./src が /src にマッピングされたので /src/my_app.rb が参照できたっていうところだけわかった。

canvas に描画する

<script src="https://cdn.jsdelivr.net/npm/ruby-3_2-wasm-wasi@2.1.0/dist/browser.script.iife.js"></script>
<canvas id="my_canvas" width="800" height="600"></canvas>
<script type="text/ruby">
  require "js"
  document = JS.global[:document]
  canvas = document.getElementById("my_canvas")
  p canvas[:width]
  p canvas["width"]
  ctx = canvas.getContext("2d")
  ctx[:fillStyle] = "lightblue"
  p ctx[:fillStyle]
  ctx.fillRect(0, 0, 100, 100)
</script>

  • require "js" で JS が使えるようになる
  • ctx.fillStylectx.fillStyle= はエラーになる
  • ハッシュのように扱わないといけない

あっちの世界を参照する

<script src="https://cdn.jsdelivr.net/npm/ruby-3_2-wasm-wasi@2.1.0/dist/browser.script.iife.js"></script>
<script type="text/ruby">
  require "js"
  p JS.eval("return Math.PI")
  p JS.global["Math"]["PI"]
</script>
  • JS.eval で取ってこれる
    • が、nil しか返ってこなくてはまる (return をつけるのを忘れるな)
  • global でも取ってこれる
    • JS.global.Math と書くとエラーになる
    • global に続けて書けるのはメソッドのときだけっぽい
  • JS.global["Math"]["PI"] * 2 はエラーになる
    • 演算するには、いったん to_f しないといけない

クリックカウンタ

<script src="https://cdn.jsdelivr.net/npm/ruby-3_2-wasm-wasi@2.1.0/dist/browser.script.iife.js"></script>
<button>0</button>
<script type="text/ruby">
  require "js"
  el = JS.global[:document].querySelector("button")
  el.addEventListener("click") do |e|
    e[:target][:innerText] = e[:target][:innerText].to_i.next.to_s
  end
</script>
  • 普通にブロックを書けるのが嬉しい
  • やっぱり to_i は必要

メソッドだけ普通に呼べるのはなぜ?

https://github.com/ruby/ruby.wasm/blob/99f50000679a72f6f023e07d8181801a49abf395/ext/js/lib/js.rb#L165-L176

function なら call するようになっていた。

ついでに xxx?xxx() == true の結果を返すんだろうか (?)

なんでも to_i や to_s しないといけない問題

こちらでは method_missing の改良でいい感じにしていた。

https://ongaeshi.hatenablog.com/entry/access_properties_from_jsobject_in_function_style

HTML の中に Ruby のコードを書きたくない場合

<script src="https://cdn.jsdelivr.net/npm/ruby-3_2-wasm-wasi@2.1.0/dist/browser.script.iife.js"></script>
<script type="text/ruby" src="hello.rb"></script>

としたら hello.rb が読み込めずにはまった (初歩的)

file:// からのフルパスにしても CORS エラーとか言われるので

ruby -rwebrick -e 'WEBrick::HTTPServer.new(DocumentRoot: "./", Port: 3000).start'

とした。しかし今度はファイルの更新が反映されず、

<script src="https://cdn.jsdelivr.net/npm/ruby-3_2-wasm-wasi@2.1.0/dist/browser.script.iife.js"></script>
<script type="text/ruby" src="hello.rb"></script>
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Cache-Control" content="no-cache" />

としたらやっと反映された。このあたりはこちらが詳しかった。

https://diggle.engineer/entry/rubykaigi2023_game

Promise 関連

これは

<script>
  async function func() {
    const response = await fetch("https://yesno.wtf/api")
    const data = await response.json()
    console.log(data)
  }
  func()
</script>

こう書く。

<script src="https://cdn.jsdelivr.net/npm/ruby-3_2-wasm-wasi@2.1.0/dist/browser.script.iife.js"></script>
<script type="text/ruby" data-eval="async">
  require "js"
  response = JS.global.fetch("https://yesno.wtf/api").await
  data = response.json.await
  p data
  p data[:image]
</script>
  • JS.global[:fetch]("https://yesno.wtf/api") は構文エラー
  • p data としても [object Object] と表示されるだけでハッシュの中身は見えない
  • image 属性があるのを知っていないといけない

このあたりはこちらが詳しかった。

https://speakerdeck.com/lnit/ruby-wasm-2023-matsuerubykaigi?slide=65

Discussion