RubyのFloat(arg, exception: true)をbuiltinで再実装してみた

# はじめに

この記事は以前書いた以下の記事で紹介したbuiltinを使って`Float(arg, exception: true)`というメソッドを再定義してみた記事になります。

[Ruby(とC)でRubyを実装してみた（builtinで遊んでみた）](https://qiita.com/S_H_/items/452c94d6e832789f17ad)

やっていることは前回の記事とおおよそ同じです。前回と異なる点は、特定のクラスのメソッドではなく、どこでも呼び出すことができるメソッドをbuiltinで再定義してみたという点です。

前回の記事を書いた後、`raise`や`Float`などのメソッドをbuiltin対応ができるのか気になり試してみた感じですね。

結論から言うと、そういったグローバルなメソッドもbuiltinで対応することができるようです(意図した挙動かは確認とっていないので、推奨されたものではないかもしれませんが……）

# builtinって？

builtinとはRuby(とC)でRuby自体を実装するというものです。詳しくは前回の記事を読んでいただければと思います。

[Ruby(とC)でRubyを実装してみた（builtinで遊んでみた）](https://qiita.com/S_H_/items/452c94d6e832789f17ad)

# 環境構築

前回の記事の環境をそのまま使用しています。特に大きな変更点などもありません。

[Ruby(とC)でRubyを実装してみた（builtinで遊んでみた）](https://qiita.com/S_H_/items/452c94d6e832789f17ad)

# やってみた
## Floatメソッドの実装場所を探す

まずは`Float`メソッドが実装されているソースを探します。Rubyのメソッドは大体こんなかんじで定義されています。

```c
rb_define_method(rb_mKernel, "to_s", rb_any_to_s, 0);
```

第一引数がメソッドが定義されているクラス/モジュールの変数になります。大体、`rb_cArray`などのようにクラス名やモジュール名で作成されています。第二引数の`"to_s"`はRuby側で呼び出されるメソッド名になっています。第三引数はメソッドが呼び出された際に実行するCの関数で、最後の引数がメソッドが受け取る引数の数を指定しています。

今回、再実装したいのは`Float`メソッドですのでRubyのソースコード内から`git grep \"Float\"`などで検索してみましょう。
すると`object.c`で以下のように実装されているようです。

```c
rb_define_global_function("Float", rb_f_float, -1);
```

それでは再実装していきます。

## Floatメソッドの再実装

前回の記事では`Hash#delete`を再実装していました。またbuiltin対応のため、`common.mk`などの修正していました。
ですが、現在のRubyのmasterのコードでは`object.c`で実装されていた`Kernel#clone`をbuiltinに対応させているので、それらの修正は必要ありません。

修正が必要なソースは`object.c`と`kernel.rb`の二つになります。

## object.cの修正

`object.c`ではまず`Float`メソッドを定義している以下のコードを削除します。

```diff
void
InitVM_Object(void)
{
    Init_class_hierarchy();

    // 省略
-    rb_define_global_function("Float", rb_f_float, -1);
    // 省略
}
```

次に、`Float`メソッドの処理を定義している`rb_f_float`を以下のように修正します。

```diff
- /*	
-  *  call-seq:	
-  *     Float(arg, exception: true)    -> float or nil	
-  *	
-  *  Returns <i>arg</i> converted to a float. Numeric types are	
-  *  converted directly, and with exception to String and	
-  *  <code>nil</code> the rest are converted using	
-  *  <i>arg</i><code>.to_f</code>.  Converting a String with invalid	
-  *  characters will result in a ArgumentError.  Converting	
-  *  <code>nil</code> generates a TypeError.  Exceptions can be	
-  *  suppressed by passing <code>exception: false</code>.	
-  *	
-  *     Float(1)                 #=> 1.0	
-  *     Float("123.456")         #=> 123.456	
-  *     Float("123.0_badstring") #=> ArgumentError: invalid value for Float(): "123.0_badstring"	
-  *     Float(nil)               #=> TypeError: can't convert nil into Float	
-  *     Float("123.0_badstring", exception: false)  #=> nil	
-  */	

static VALUE	static VALUE
- rb_f_float(int argc, VALUE *argv, VALUE obj)
+ rb_f_float(rb_execution_context_t *ec, VALUE main, VALUE arg, VALUE opts)
{
-    VALUE arg = Qnil, opts = Qnil;	
-
-    rb_scan_args(argc, argv, "1:", &arg, &opts);	
-    return rb_convert_to_float(arg, opts_exception_p(opts));	    
+   return rb_convert_to_float(arg, opts_exception_p(opts));
}
```

builtinを使い、キーワード引数を受け取るので`int argc, VALUE *argv`ではなく`VALUE main, VALUE arg, VALUE opts`としています。また`VALUE main`はグローバルなメソッドが暗黙の裡に受け取っている引数を代わりに受け取るために追加しています。

これで`object.c`での修正は完了です！

## kernel.rbの修正

Rubyでは、どこからでも呼び出すことができるグローバルなメソッドは`Kernel`モジュールにて再定義することができます。

たとえば、`puts`メソッドは以下のようにモンキーパッチすることができます。

```ruby
module Kernel
   def puts *args
      p "hoge" 
   end
end


puts :ho
#=> "hoge" と表示される
```

これを使い、`kernel.rb`内に以下のように`Float`メソッドを追加します。

```ruby
module Kernel
+ 
+   #
+   #  call-seq:
+   #     Float(arg, exception: true)    -> float or nil
+   #
+   #  Returns <i>arg</i> converted to a float. Numeric types are
+   #  converted directly, and with exception to String and
+   #  <code>nil</code> the rest are converted using
+   #  <i>arg</i><code>.to_f</code>.  Converting a String with invalid
+   #  characters will result in a ArgumentError.  Converting
+   #  <code>nil</code> generates a TypeError.  Exceptions can be
+   #  suppressed by passing <code>exception: false</code>.
+   #
+   #     Float(1)                 #=> 1.0
+   #     Float("123.456")         #=> 123.456
+   #     Float("123.0_badstring") #=> ArgumentError: invalid value for Float(): "123.0_badstring"
+   #     Float(nil)               #=> TypeError: can't convert nil into Float
+   #     Float("123.0_badstring", exception: false)  #=> nil
+   #
+   def Float(arg, exception: true)
+     __builtin_rb_f_float(arg, exception)
+   end
+ 
  #
  #  call-seq:
  #     obj.clone(freeze: nil) -> an_object
  #
  #  Produces a shallow copy of <i>obj</i>---the instance variables of
  #  <i>obj</i> are copied, but not the objects they reference.
  #  #clone copies the frozen value state of <i>obj</i>, unless the
  #  +:freeze+ keyword argument is given with a false or true value.
  #  See also the discussion under Object#dup.
  #
  #     class Klass
  #        attr_accessor :str
  #     end
  #     s1 = Klass.new      #=> #<Klass:0x401b3a38>
  #     s1.str = "Hello"    #=> "Hello"
  #     s2 = s1.clone       #=> #<Klass:0x401b3998 @str="Hello">
  #     s2.str[1,4] = "i"   #=> "i"
  #     s1.inspect          #=> "#<Klass:0x401b3a38 @str=\"Hi\">"
  #     s2.inspect          #=> "#<Klass:0x401b3998 @str=\"Hi\">"
  #
  #  This method may have class-specific behavior.  If so, that
  #  behavior will be documented under the #+initialize_copy+ method of
  #  the class.
  #
  def clone(freeze: nil)
    __builtin_rb_obj_clone2(freeze)
  end
end

```

先ほどbuiltin対応させた`rb_f_float`を`__builtin_rb_f_float`で呼び出しています。これで`kernel.rb`での修正はOKです。

## 最後に

あとは`make`と`make install` を実行してビルドできればbuiltinでの実装は完了です。

# おわりに

こんな感じで`Float`や`Integer`などのメソッドをbuiltin対応できそうです。パッチとして送るかは別として、結構応用できそうだったのが面白かったですね。

ちなみに、以下のようなベンチマークを試してみたところパフォーマンス向上は期待できそうにないかなという感じでした。

```yml
benchmark:
  float: "Float(42)"
  float_true: "Float(42, exception: true)"
  float_false: "Float(42, exception: false)"
loop_count: 10000

```

以下結果

```bash
Calculating -------------------------------------
                     compare-ruby  built-ruby
               float      37.495M     15.878M i/s -     10.000k times in 0.000267s 0.000630s
          float_true       1.109M      1.211M i/s -     10.000k times in 0.009015s 0.008261s
         float_false       1.227M      1.191M i/s -     10.000k times in 0.008150s 0.008395s

Comparison:
                            float
        compare-ruby:  37495314.0 i/s
          built-ruby:  15878056.0 i/s - 2.36x  slower

                       float_true
          built-ruby:   1210507.2 i/s
        compare-ruby:   1109250.0 i/s - 1.09x  slower

                      float_false
        compare-ruby:   1226948.7 i/s
          built-ruby:   1191128.5 i/s - 1.03x  slower
```

仮に`Float`などのメソッドをbuiltin対応する場合はパフォーマンスよりもコードのメンテナンス性などを重視することになるのかなと思いますね。