OpalのNativeモジュール

なんかOpalのドキュメントにも見当たらないのでコードを見ながら機能の勉強。勉強中なのでウソが書いてあるかもしれない。
とりあえずクラスとモジュールの定義部分だけ引っこ抜いてみる。

module Native
  module Helpers
module Kernel
class Native::Object < BasicObject
  include ::Native
class Native::Array
  include Native
  include Enumerable
class Numeric
class Proc
class String
class Regexp
class MatchData
class Struct
class Array
class Boolean
class Time
class NilClass
class Hash
class Module
class Class

このうち、NumericからHashまではわりとシンプルにto_nメソッドを追加しているだけだ。ただし、StructとHashについてはinitializeを上書きしてto_n用のデータを作っているように見える。to_nというのはOpalのNativeモジュール独自メソッドで、RubyオブジェクトをJavaScriptオブジェクトに変換する。基本的にはNative.callなどでJavaScriptオブジェクトのメソッドを呼ぶ際に引数の変換に使われる。
それはさておき、Nativeの中核はKernelに追加される少数のメソッドとNativeモジュール、そしてNative::Objectクラスである。

Kernelで定義されるメソッド

Kernel#native?、Kernel#Native、Kernel#Arrayの3つ。

native?(value)

Rubyオブジェクトじゃない場合にtrueを返す。判定条件は、JavaScriptレベルでnullの場合、もしくは$$classが定義されていない場合。なのでうっかり$$classという名称を使わないように注意する必要がある。$$classというのはOpalが定義するプロパティでRubyのクラスが入っているんじゃないかと思う。たぶん。

Native(obj)

引数で渡されたJavaScriptオブジェクトをNative::Objectでラップして返す。ただし、nullの場合はnilに変換するし、native?がfalseの場合はそのまま返す。

Array(object, *args, &block)

RubyのマイナーなメソッドKernel#Arrayを上書きし、native?がtrueの場合はNative::Arrayを返すようにする機能追加。

まあ、一般的によく使うであろうメソッドはNativeだけである。

Nativeモジュール

Nativeモジュールにはinitializeとto_nが定義されていて、Native::ObjectおよびNative::Arrayにincludeされる。Nativeモジュール内のHelpersモジュールはでincludeされたときに更に追加で自動的にextendされるようself.includedが定義されているっぽい。他、様々なメソッドが定義されている。

initialize(native)

引数に対するKernel#native?がfalseだと例外を投げる。Native::ObjectとNative::Arrayも、newの引数はJavaScriptオブジェクトである必要がある。また、@nativeに引数のオブジェクトを格納する。

to_n

@nativeを返す。

Native.is_a?(object, klass)

objectとklassを引数に取り、instanceofでプロトタイプチェーンを調査する。

Native.try_convert(value)

native?がtrueならそのまま返す。to_nできたらto_nする。できなければnilを返す。

Native.convert(value)

try_convertに対して、変換できなければ例外。

Native.call(obj, key, *args, &block)

送信先オブジェクト、メソッド名、引数を渡すとJavaScriptの同名のメソッドを呼び出してくれる。このときに引数はすべてto_nされるが変換できなければ元のオブジェクトが渡される。ブロックを渡した場合はそれが引数の最後に追加される。また、呼び出し対象がメソッドじゃなくてプロパティだった場合は引数を無視して値を取得する。返ってきた値はNative()によりNative::Object化される。このロジックからするとプロパティへの代入はこのメソッドでは実行できないので、おそらく内部用でありユーザの使用は推奨されない。

Helpersモジュールについてはよくわからない。JavaScriptメソッドをRubyから呼べるようにするっぽいalias_nativeやプロパティのアクセサを定義するメソッドがあるのだが、使われていないし使い方の例も見当たらない。alias_nativeについてはOpalランタイム上でModuleクラスにもっと簡素なものが定義されている。

Native::Object

Native(jsobj)で返される、jsobjをラップしたオブジェクトのクラス。JavaScriptオブジェクトをRubyから扱う場合に便利な機能がいくつか定義されているが、主なものだけリストアップ。

[](key)

JavaScriptオブジェクトの属性(って言うのか?)を取得する。プロパティなら値が、FunctionならFunctionオブジェクトが返ってくる。値の取得はNative::Object化されるがFunctionの場合はされない。

[]=(key, value)

JavaScriptオブジェクトの属性に値をセットする。が、Native.try_convertして`#{native} === nil`で判定していて、これが何を意味するのかがわからない。JavaScriptオブジェクトだったらそのまま、Rubyだったらto_nしているのかもしれない。

method_missing(mid, *args, &block)

これが最大のミソか。RubyのメソッドはJavaScriptから見ると$付きなので直接呼べず、Rubyに定義されていない名前のメソッドを呼んだときに$をはずして呼び出しを試みる。この場合、名称の最後が=だったらプロパティへの書き込みを行い、違えばNative.callを呼ぶ。このような形になっているのでNative.callはプロパティへの代入をサポートしていない、ということだ。

おしまい

Native::ArrayもあるけどJavaScriptの配列についてよく知らないので保留。ClassとModuleは1個2個メソッドが定義されているが、何をやっているのかよくわからないので保留。
ところでQiitaにOpalタグがあって、見てみるとyharaさんばっかりなんだけども、Raphael.jsの記事githubに置いてあるコードが興味深い。抜粋すると

  class Element < `Raphael.el.constructor`
    alias_native :animate

これなのだが、JavaScriptのオブジェクトをスーパークラスにしてRubyのクラスを定義するってこれいったい何が起こってんの?