はじめに
この記事は以前書いた以下の記事で紹介したbuiltinを使ってFloat(arg, exception: true)
というメソッドを再定義してみた記事になります。
やっていることは前回の記事とおおよそ同じです。前回と異なる点は、特定のクラスのメソッドではなく、どこでも呼び出すことができるメソッドをbuiltinで再定義してみたという点です。
前回の記事を書いた後、raise
やFloat
などのメソッドをbuiltin対応ができるのか気になり試してみた感じですね。
結論から言うと、そういったグローバルなメソッドもbuiltinで対応することができるようです(意図した挙動かは確認とっていないので、推奨されたものではないかもしれませんが……)
builtinって?
builtinとはRuby(とC)でRuby自体を実装するというものです。詳しくは前回の記事を読んでいただければと思います。
環境構築
前回の記事の環境をそのまま使用しています。特に大きな変更点などもありません。
やってみた
Floatメソッドの実装場所を探す
まずはFloat
メソッドが実装されているソースを探します。Rubyのメソッドは大体こんなかんじで定義されています。
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
で以下のように実装されているようです。
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
メソッドを定義している以下のコードを削除します。
void
InitVM_Object(void)
{
Init_class_hierarchy();
// 省略
- rb_define_global_function("Float", rb_f_float, -1);
// 省略
}
次に、Float
メソッドの処理を定義しているrb_f_float
を以下のように修正します。
- /* - * 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
メソッドは以下のようにモンキーパッチすることができます。
module Kernel def puts *args p "hoge" end end puts :ho #=> "hoge" と表示される
これを使い、kernel.rb
内に以下のようにFloat
メソッドを追加します。
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対応できそうです。パッチとして送るかは別として、結構応用できそうだったのが面白かったですね。
ちなみに、以下のようなベンチマークを試してみたところパフォーマンス向上は期待できそうにないかなという感じでした。
benchmark: float: "Float(42)" float_true: "Float(42, exception: true)" float_false: "Float(42, exception: false)" loop_count: 10000
以下結果
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対応する場合はパフォーマンスよりもコードのメンテナンス性などを重視することになるのかなと思いますね。