Posted at
OpalDay 24

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

More than 1 year has passed since last update.

この記事は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コードへの書きかえ

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

メリークリスマス!!!