LoginSignup
1

More than 5 years have passed since last update.

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

Posted at

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

今日はNativeの実装について書こうと思います。
その前にNativeのつかいかたです。

require 'native'

%x{
  var foo = new Object();
  foo.bar = function() {
    return 'bar';
  };
  foo.baz = 'baz';
}

n = Native(`foo`)
puts n.bar
puts n.baz

こんなプログラムを実行してみましょう。n.barn.bazもそれぞれ文字列を返します。
Rubyのこれらの呼び出しはすべてメソッド呼び出しになります。
一方、JavaScriptではプロパティに関数だったり、文字列だったりをアサインすることができます。
Nativeは関数であれば関数を実行した結果を返すし、文字列などの値であればその値を返すようになっています。
それでは、これらがどのように実装されているか見てみましょう。

Nativestdlib/native.rbに定義されています。

まずはNative()の定義をみてみます。

  def Native(obj)
    if `#{obj} == null`
      nil
    elsif native?(obj)
      Native::Object.new(obj)
    else
      obj
    end
  end

nativeであればNative::Objectを返します。native?はどのように判定しているのでしょうか?

  def native?(value)
    `value == null || !value.$$class`
  end

nullの場合もしくは、$$classが定義されていない場合はnativeであると判定しています。
$$classはRubyのクラスを表す変数です。この定義があればRubyのクラスから生成されたオブジェクトと言えます。

Native::Objectのメソッド呼び出しはmethod_missingでnativeの呼び出しに変換されます。

  def method_missing(mid, *args, &block)
    %x{
      if (mid.charAt(mid.length - 1) === '=') {
        return #{self[mid.slice(0, mid.length - 1)] = args[0]};
      }
      else {
        return #{::Native.call(@native, mid, *args, &block)};
      }
    }
  end

メソッド名の最後が=だったらプロパティへの代入になります。
Native::Object#[]はJSのプロパティを返します。

  def [](key)
    %x{
      var prop = #@native[key];

      if (prop instanceof Function) {
        return prop;
      }
      else {
        return #{::Native.call(@native, key)}
      }
    }
  end

メソッド名が=で終らない場合は、Native#callが呼ばれます。

  def self.call(obj, key, *args, &block)
    %x{
      var prop = #{obj}[#{key}];

      if (prop instanceof Function) {
        var converted = new Array(args.length);

        for (var i = 0, length = args.length; i < length; i++) {
          var item = args[i],
              conv = #{try_convert(`item`)};

          converted[i] = conv === nil ? item : conv;
        }

        if (block !== nil) {
          converted.push(block);
        }

        return #{Native(`prop.apply(#{obj}, converted)`)};
      }
      else {
        return #{Native(`prop`)};
      }
    }
  end

Native#callの中身はプロパティが関数の場合は引数をnativeに変換して関数を呼び出します。
関数の場合は関数の呼び出し結果、それ以外ならプロパティそのものをNative::Objectでラップして返します。

引数の変換はto_nメソッドでおこなわれます。

  def self.try_convert(value)
    %x{
      if (#{native?(value)}) {
        return #{value};
      }
      else if (#{value.respond_to? :to_n}) {
        return #{value.to_n};
      }
      else {
        return nil;
      }
    }
  end

to_nメソッドはnative.rbのなかでいくつか定義されています。
たとえば、Numericなら

class Numeric
  def to_n
    `self.valueOf()`
  end
end

valueOf()でプリミティブを返すようになっています。

native.rbにはほかにもさまざまなクラスにto_nの定義をしています。ArrayHashなどもあります。
もちろん、Native::Object自身もto_nメソッドをもちます。
しかし、to_nが定義されていないクラスに関しては、nativeへの変換は行なわれませんので注意が必要です。
逆にto_nを定義すればnativeのメソッドに引数として渡すことができるようになります。

Nativeの実装についてみてきましたがいかがでしょうか。次回はJSの実装を見てみましょう。
では、Happy hacking!

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1