1. Qiita
  2. Items
  3. Ruby

OpalのJSはどのように実装されているのか

  • 3
    Like
  • 0
    Comment

この記事はOpal Advent Calendar 2016の24日目の投稿として書いています。

今日はクリスマスイブですね。昨日、予告したとおり今日はJSの実装について書きます。
例によって使いかたからです。

require 'js'

%x{
  function Foo() {
    this.bar = function() {
      return 'bar';
    };
    this.baz = 'baz';
  };
}

foo = JS.new(`Foo`)
puts foo.JS.bar
puts foo.JS[:baz]

昨日のサンプルと似ていますね。どちらもJavaScriptのコードを呼びだすためのAPIということがわかります。
NativeはRubyのオブジェクトでラップしてRubyのオブジェクトとして扱えるようにしたものですが、JSはJavaScriptのオブジェクトを直接操作するためにつくられています。

foo = JS.new(`Foo`)

この呼び出しで返ってくるオブジェクト(foo)はRubyのオブジェクトではなく、JavaScript側のオブジェクトとして返ってきます。
つまり

foo = `new Foo()`

と書くのと等価です。(後で見ますが実際にはapplyをつかって呼び出しています。)

fooはJavaScriptのオブジェクトなので、Rubyのメソッドとしてbarを呼ぶことができません。
(Nativeではmethod_missingを利用してbarを呼ぶとJavaScriptのbar()関数が呼び出されました。)
そこで、RubyのなかでJavaScriptのbar()関数を呼ぶには、

foo.JS.bar

のようにJSというメソッド(?)を介して呼びだします。
このコードは

`foo.bar()`

と等価になります。

それでは、stdlib/js.rbのコードを見てみましょう。
newの実装はこのようになっています。

  if `typeof Function.prototype.bind == 'function'`
    def new(func, *args, &block)
      args.insert(0, `this`)
      args << block if block
      `new (#{func}.bind.apply(#{func}, #{args}))()`
    end
  else
    def new(func, *args, &block)
      args << block if block
      f = `function(){return func.apply(this, args)}`
      f.JS[:prototype] = func.JS[:prototype]
      `new f()`
    end
  end

bindが定義されているかどうかで分岐していますが、基本的にはコンストラクタをapplyしてnewするという感じです。
newの他にも、delete,in,instanceof,typeofが定義されていますが、単純にJavaScriptのコードを呼ぶだけになっています。
また、callというメソッドも用意されていて、グローバルな関数を呼び出すことができます。

  def call(func, *args, &block)
    g = global
    args << block if block
    g.JS[func].JS.apply(g, args)
  end
  alias method_missing call

これはmethod_missingにaliasされているので、明示的にcallしなくても、JS.parseInt()のように呼びだすこともできます。

このようなJSモジュールに定義されるメソッドはstdlib/js.rbに定義されていますが、foo.JS.barのようなものはどのように実装されているのでしょうか。
実はstdlib/js.rbにはこの実装はありません。ためしにrequire 'js'をしなくてもこれらのコードはちゃんと実行できます。
ではどこにあるのでしょうか?

先程、

foo.JS.bar

`foo.bar()`

と等価と書きましたが、実際にコンパイルされたコードをみるとまったく同じにコンパイルされています。
ということはJSというのはどこに行ってしまったのでしょう?

答えはOpal::Rewriters::DotJsSyntaxというクラスにあります。(lib/opal/rewriters/dot_js_syntax.rb)

lib/opal/rewriters/dot_js_syntax.rb
      def on_send(node)
        recv, meth, *args = *node
        if recv && recv.type == :send
          recv_of_recv, meth_of_recv, _ = *recv
          if meth_of_recv == :JS
            case meth
            when :[]
              if args.size != 1
                raise SyntaxError, ".JS[:property] syntax supports only one argument"
              end
              property = args.first

              node = to_js_attr_call(recv_of_recv, property)
            when :[]=
              if args.size != 2
                raise SyntaxError, '.JS[:property]= syntax supports only two arguments'
              end

              property, value = *args
              node = to_js_attr_assign_call(recv_of_recv, property, value)
            else
              node = to_native_js_call(recv_of_recv, meth, args)
            end
            super(node)
          else
            super
          end
        else
          super
        end
      end

Opalのコンパイラはrecv.JSというコードに対して書き替えをおこなうようです。JSの後に続く文字列によって、プロパティへのアクセスあるいはアサイン、メソッドコールの3パターンに分れます。
それぞれ、to_js_attr_call,to_js_attr_assign_call,to_native_js_callを呼びだしています。

たとえば、to_js_attr_callだったら次のように定義されています。

      def to_js_attr_call(recv, property)
        s(:jsattr, recv, property)
      end

Opalのパーサーはs式を模した構文木を作るので、sというメソッドの呼びだしになっています。
ここでは、:jsattrというノードをつくります。
このノードがコードを生成するのは、lib/opal/nodes/call_special.rbに定義されています。

lib/opal/nodes/call_special.rb
    # recvr.JS[:prop]
    # => recvr.prop
    class JsAttrNode < Base
      handle :jsattr
      children :recvr, :property

      def compile
        push recv(recvr), '[', expr(property), ']'
      end
    end

JSという一見同じ機能に見えたものは二つの機能によって実現されていました。

  • JSモジュールによる、JavaScriptのグローバルな関数や演算子の利用
  • recv.JSのようなメソッド呼び出しのnativeコードへの書きかえ

この記事を書きはじめたときにはイブだったのですが、とうとう日付が変ってしまいました。
メリークリスマス!!!