🗿
はじめての 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
ドキュメントを読む感じ、wasi-vfs は Ruby 専用というわけではないらしい。
こちらが詳しかった。
wasmtime とは? (3行で)
- WebAssembly 用のランタイム
- WASI の仕様に沿って実装されたもの
- Bytecode Aliance (非営利団体) が作っている
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 で実行する
の手順通りにやってみる。
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.fillStyle
やctx.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 は必要
メソッドだけ普通に呼べるのはなぜ?
function なら call するようになっていた。
ついでに xxx?
は xxx() == true
の結果を返すんだろうか (?)
なんでも to_i や to_s しないといけない問題
こちらでは method_missing の改良でいい感じにしていた。
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" />
としたらやっと反映された。このあたりはこちらが詳しかった。
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 属性があるのを知っていないといけない
このあたりはこちらが詳しかった。
Discussion