numeric.cを書き換えてFizzBuzzを1行で

Rubyってオープンクラスだから実行時にコアクラスを書き換えていいんですね!というネタとして、以下のように書いてみた。

require 'fileutils'

File.open("./numeric.c", "r") do |klass|
  File.open("./numeric.tmp", "w") do |tmpfile|
    while line = klass.gets
      tmpfile.write line
      if line =~ /fix_to_s\(int argc, VALUE \*argv, VALUE x\)/
        2.times {tmpfile.write klass.readline}
        tmpfile.write <<PATCH
    int val;
    val = FIX2INT(x);
    if (val % 15 == 0) return rb_str_new2("FizzBuzz");
    if (val %  3 == 0) return rb_str_new2("Fizz");
    if (val %  5 == 0) return rb_str_new2("Buzz");
PATCH
      end
    end
  end
end

FileUtils.mv("numeric.tmp", "numeric.c")

system('make')
system('./ruby -ve "(1..100).each{|i| puts i}"')

書き換えた箇所は、

/*
 *  call-seq:
 *     fix.to_s(base=10)  ->  string
 *
 *  Returns a string containing the representation of <i>fix</i> radix
 *  <i>base</i> (between 2 and 36).
 *
 *     12345.to_s       #=> "12345"
 *     12345.to_s(2)    #=> "11000000111001"
 *     12345.to_s(8)    #=> "30071"
 *     12345.to_s(10)   #=> "12345"
 *     12345.to_s(16)   #=> "3039"
 *     12345.to_s(36)   #=> "9ix"
 *
 */
static VALUE
fix_to_s(int argc, VALUE *argv, VALUE x)
{
    int base;
    int val;
    val = FIX2INT(x);
    if (val % 15 == 0) return rb_str_new2("FizzBuzz");
    if (val %  3 == 0) return rb_str_new2("Fizz");
    if (val %  5 == 0) return rb_str_new2("Buzz");

    if (argc == 0) base = 10;
    else {
        VALUE b;

        rb_scan_args(argc, argv, "01", &b);
        base = NUM2INT(b);
    }

    return rb_fix2str(x, base);
}

とかなる。

自明でくだらないと思ったけど、やってみるといろいろ分かるもの。

  • あるファイルの途中に何かを書き加えるという処理は、File.open("file.txt", "r+")では実は面倒なので別ファイルを用意して後からファイルごと入れ替えるほうがイマドキっぽい
  • IO#readlineはEOFで例外を上げるけど、IO#getsはfalseを返す
  • CRuby内部で使われるStringのコンストラクタには何種類かあって、rb_str_new2はC文字列を渡すと、そのstrlenを取って、本物の(?)rb_str_newに渡してくれる。USASCIIの場合はrb_usascii_str_newというのもある。rb_str_new2はrb_str_cstrの別名としてdefineされていて、これは歴史的経緯っぽい
  • Rubyって本体を壊して遊ぶのも楽しい