@ledsun blog

無味の味は佳境に入らざればすなわち知れず

ruby.wasmのJS::Objectをハックしてみる

ruby.wasmでJavaScriptのオブジェクトのプロパティの操作を工夫する - @ledsun blog で考えたJS::Objectに対するパッチをいれてビルドしてみます。

まずテストコードを探しました。

https://github.com/ruby/ruby.wasm/blob/394841d142fabc2287e7f918a605c7009e545846/packages/npm-packages/ruby-wasm-wasi/test/unit/test_object.rb#L185-L188

に、テストコードがありました。

def test_method_missing
  assert_equal "42", JS.eval("return 42;").toString.to_s
  assert_equal "o", JS.eval("return 'hello';").charAt(4).to_s
end

まずはプロパティのショートハンドを入れたいのでassert_equal "5", JS.eval("return 'hello';").length.to_sこんな感じのアサーションを足すと良さそうです。 テストを実行して失敗させてみます。 rake npm:ruby-head-wasm-wasi:checkを実行します。

期待通りにテストが失敗します。

https://github.com/ruby/ruby.wasm/blob/394841d142fabc2287e7f918a605c7009e545846/ext/js/lib/js.rb#L92-L98 を変更します。

def method_missing(sym, *args, &block)
  if self[sym].typeof == "function"
    self.call(sym, *args, &block)
  elsif args.empty?
    # If no argument, treat as reading a property
    # Return undefiend if a nonexistent property is read.
    self.method(:[]).call(sym)
  else
    super
  end
end

こんな感じです。

rake npm:ruby-head-wasm-wasi:checkを実行します。 テストが失敗します。 おや?ビルドが必要なのでしょうか?

rake npm:ruby-head-wasm-wasi
rake npm:ruby-head-wasm-wasi:check

を実行してみます。 テストが失敗します。 まあ、npmパッケージをビルドしてもRubyスクリプトの部分が変わらないというのはありそうです。

https://github.com/ruby/ruby.wasm/blob/main/CONTRIBUTING.md#building-and-testing-ruby-wasm-wasiをみるともう一手順ありました。

rake build:head-wasm32-unknown-wasi-full-js-debug
rake npm:ruby-head-wasm-wasi
rake npm:ruby-head-wasm-wasi:check

を実行してみます。 テストが失敗します。 おや?どうすればいいのでしょうか? ソースコードを眺めてみてもext/js/lib/js.rbがどうやって、ruby.wasmに取り込まれているのかさっぱりわかりません。

そういえばrake build:head-wasm32-unknown-wasi-full-js-debugが一瞬で終わっていました。 CRubyをソースコードからビルドし直す必要があるのでしょうか?

rake build:clean
rake npm:ruby-head-wasm-wasi
rake npm:ruby-head-wasm-wasi:check

を実行してみます。 時間かかるー。

違うテストが失敗しました。

https://github.com/ruby/ruby.wasm/blob/394841d142fabc2287e7f918a605c7009e545846/packages/npm-packages/ruby-wasm-wasi/test/unit/test_object.rb#L210-L215

def test_method_missing_with_undefined_method
  object = JS.eval(<<~JS)
    return { foo() { return true; } };
  JS
  assert_raise(NoMethodError) { object.bar }
end

ですね。なるほど、もともとの動きを変えたところです。 引数のないメソッド呼び出しをプロパティの呼び出しと読み替えたため、NoMethodErrorが出なくなりました。

assert_raise(NoMethodError) { object.bar 0 }のように引数をつけてあげます。 rake npm:ruby-head-wasm-wasi:checkを実行します。

テストに成功しました。

ハックする手順がわかりました。

もうちょっと早いサイクルで試せると嬉しいです。 まず適当にそれっぽいディレクトリを消してみます。

rm -rf build/wasm32-unknown-wasi/head-wasm32-unknown-wasi-full-js-debug-ext
rake npm:ruby-head-wasm-wasi
rake npm:ruby-head-wasm-wasi:check

を実行します。 ビルドされないようです。

rm -rf rubies/head-wasm32-unknown-wasi-full-js-debug
rake npm:ruby-head-wasm-wasi

を実行します。 エラーがおきました。 中途半端なファイル削除になっているようです。

rm -rf rubies
rake npm:ruby-head-wasm-wasi
rake npm:ruby-head-wasm-wasi:check

を実行します。 ビルドされました。 あれ?さっきより早いです。

で、やってて気が付いたのはassert_raise(NoMethodError) { object.bar() }は成り立ちません。 このときはプロパティではなくて、メソッドの呼び出しとして扱って欲しいです。 こんなのは実現出来るのでしょうか? Rubyの世界からは判定できなさそうに思えます。

20230622 追記

コミットを公開しました。

https://github.com/ledsun/ruby.wasm/commit/bd4cce8f6b88466d5943f6207721d634d2f1c603