1. Qiita
  2. 投稿
  3. Ruby

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

  • 0
    いいね
  • 0
    コメント

    この記事は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!