Posted at
RubyDay 12

pry-docでカジュアルにRubyのソースコードを読む

More than 5 years have passed since last update.

RubyのREPL環境として一般的なpryというgemがあります。

pryの非常に便利な機能としてshow-sourceというコマンドがあり、指定したクラスやメソッドのソースコードをpry上で表示してくれます。

[15] pry(main)> require 'tsort'

true
[16] pry(main)> show-source TSort#tsort

From: /Users/joker/.rbenv/versions/2.0.0-p247/lib/ruby/2.0.0/tsort.rb @ line 133:
Owner: TSort
Visibility: public
Number of lines: 5

def tsort
result = []
tsort_each {|element| result << element}
result
end

デフォルトのままのpryはCで書かれたRuby組み込みのクラスやメソッドのソースを表示することが出来ません。

そこでpry-docをインストールすると、Cで書かれたRubyのソースコードを表示することができるようになります。

pry-docはgem内部にCのソースコードとメソッドの対応関係を示したYARDを持っていて、それを利用してソースコードを表示しています。

pry-docを利用するとこんな感じでソースコードが読めます。

[34] pry(main)> show-source Array#<<

From: array.c (C Method):
Owner: Array
Visibility: public
Number of lines: 10

VALUE
rb_ary_push(VALUE ary, VALUE item)
{
long idx = RARRAY_LEN(ary);

ary_ensure_room_for_push(ary, 1);
RARRAY_PTR(ary)[idx] = item;
ARY_SET_LEN(ary, idx + 1);
return ary;
}

なるほどArray#<<は配列の長さを取得して、push出来る領域を確保し、末尾のインデックスに直接itemを挿入して配列の長さを+1しているのか、ということが分かります。

一方でArray#<<と良く似ているけど複数引数を受け取れるArray#pushを見てみるとこんな感じです。

[39] pry(main)> show-source Array#push

From: array.c (C Method):
Owner: Array
Visibility: public
Number of lines: 5

static VALUE
rb_ary_push_m(int argc, VALUE *argv, VALUE ary)
{
return rb_ary_cat(ary, argv, argc);
}

流石にこれだけじゃ分かりませんね。でもエンドポイントとなっている関数名と実装があるファイル名が分かってるので、Rubyソースコードのどこを読めば良いかはすぐに分かります。

Rubyのソースは必要になったら落としてくれば良いですが、それだと若干面倒くさいのでrbenvを上手く活用すると捗ります。

rbenvでRubyをインストールしているなら--keep (-k)オプションを付けてインストールしておくとRubyのソースコードをrbenvのディレクトリに保存しておいてくれます。

array.cを開いてrb_ary_catとは何なのかを調べてみましょう。

rb_ary_cat(VALUE ary, const VALUE *ptr, long len)

{
long oldlen = RARRAY_LEN(ary);

ary_ensure_room_for_push(ary, len);
ary_memcpy(ary, oldlen, len, ptr);
ARY_SET_LEN(ary, oldlen + len);
return ary;
}

Array#pushary_memcpyという関数で配列の末尾から追加の長さ分コピーすることによって配列を変更しているんだな、という事が分かりますね。

ついでにary_memcpyも見てみましょう。

ary_memcpy(VALUE ary, long beg, long argc, const VALUE *argv)

{
#if 1
if (OBJ_PROMOTED(ary)) {
if (argc > (int)(128/sizeof(VALUE)) /* is magic number (cache line size) */) {
rb_gc_writebarrier_remember_promoted(ary);
RARRAY_PTR_USE(ary, ptr, {
MEMCPY(ptr+beg, argv, VALUE, argc);
});
}
else {
int i;
RARRAY_PTR_USE(ary, ptr, {
for (i=0; i<argc; i++) {
OBJ_WRITE(ary, &ptr[i+beg], argv[i]);
}
});
}
}
else {
RARRAY_PTR_USE(ary, ptr, {
MEMCPY(ptr+beg, argv, VALUE, argc);
});
}
#else
/* use shady (traditional way) */
MEMCPY(RARRAY_PTR(ary)+beg, argv, VALUE, argc);
#endif
}

MEMCPYというマクロでポインタの内容をコピーしていて、コピー対象の数が少ない時にはforループでOBJ_WRITEを呼んで配列に追加していってるようですね。

私はCをちゃんとやったことがないのですが、pry-docで取っ掛かりを掴む事で、何とか読み進めていくことが出来ます。

コア部分をいきなり読むのは難しいですが、外周部分のメソッドの呼び出しから読んでいくと、何を目的にした関数なのかはっきりしているのでかなり読み易いと思います。

Cのソースコードを読んでおくことで、呼び出した結果どれぐらいの計算が走ってるのかとか、内部でオブジェクトがコピーされている、ということがちゃんと把握できるようになります。それにCでgemを書くための参考にもなります。

pry-docから始めるRubyのソースコードリーディング、興味があれば是非やってみてください。