@ledsun blog

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

ruby.wasmでJavaScriptのオブジェクトのプロパティの操作を工夫する

いまruby.wasmでブラウザ向けプログラミングするために

<script src="https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@2.0.0/dist/browser.script.iife.js"></script>

を使うとプロパティを参照しようとすると次のように書きます。

require 'js'

documetnt = JS.global[:document]

これを次のように書きたいです。

require 'js'

documetnt = JS.global.document

次のモンキーパッチを書きました。

require 'js'

class JS::Object
  def method_missing(sym, *args, &block)
    if self[sym].typeof == "function"
      # 関数として定義されていたら、関数として呼び出す。
      self.call(sym, *args, &block)
    elsif sym.end_with? '='
      # =で終わるメソッドはセッター
      self.method(:[]=).call(sym.to_s.gsub!(/=$/, ''), *args)
    elsif args.empty?
      # 引数がなければゲッター
      # JavaScriptのオブジェクトなので、存在しないプロパティを読んでもエラーにしない。undefiendを返す。
      self.method(:[]).call(sym)
    else
      # 引数がないメソッド呼び出しは method_missing
      super
    end
  end
end

これは https://github.com/ruby/ruby.wasm/blob/394841d142fabc2287e7f918a605c7009e545846/ext/js/lib/js.rb#L92-L98 に対するパッチです。 ビルドは必要ありません。 ブラウザで動かしたいRubyスクリプトにコピペすれば動きます。

ポイントは「プロパティ参照のとき、指定した名前のプロパティが定義されていない時にエラーにしない。」です。 JavaScriptには、プロパティの定義という概念がありません。 ですので

hoge = {
  fuga: undefined
}

hoge.fuga // => undefined

hoge = {}

hoge.fuga // => undefined

に、区別がありません。 JS::ObjectはJavaScriptオブジェクトのラッパーなので、この動きを踏襲すると良さそうです。 そこで「プロパティ参照のとき、指定した名前のプロパティが定義されていない時にエラーにしない。」です。

しばらく使ってみて不満がないかどうか確かめてみます。

先行事例

ruby.wasm で await を使う - tmtms のメモ

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

プロパティ参照の実装はこちらを参考にしました。

unloose

https://github.com/aaaa777/unloosen/blob/f66ce360646aedefdd0e171e4d92a80ae3832351/lib/unloosen/utils/js.rb#L115-L132

  def method_missing(sym, *args, &block)
    ret = self[sym]

    case ret.typeof
    when "undefined"
      str = sym.to_s
      if str[-1] == "="
        self[str.chop.to_sym] = args.first
        return args.first
      end

      super
    when "function"
      self.call(sym, *args.map, &block).to_rb
    else
      ret.to_rb
    end
  end

ほとんど一緒ですが、未定義プロパティを参照したときにエラーになる点が異なります。 Ruby で Processing がブラウザ上からできる p5.rb を作りました - ブログのおんがえしの実装も同じようです。