kenju's blog

About Programming, Mathematics, Security and Blockchain

『Rubyのしくみ -Ruby Under a Microscope』の読書メモ<第1~5章>

Rubyのしくみ -Ruby Under a Microscope-

Rubyのしくみ -Ruby Under a Microscope-

第1章 字句解析と構文解析

遠くからは複雑に見えることも、間近で見るとすごく単純だったりする

字句解析

  • Rubyの字句解析の文法規則ファイルはparse.y
    • parser_yylex(struct parser_params *parser)関数が実際に字句解析を行うコード

構文解析

  • RubyはLex字句解析ツールは使用しておらず、自前の字句解析コードを用意している
  • パーサジェネレータ(一連の文法規則を入力に取り、トークン列を処理するためのパーサーを生成する)には、Yacc(Yet Another Compiler Compiler)の後継であるBisonを利用している
  • 構文解析アルゴリズムはLALR(Look-Ahead, Left to right, Rightmost derivation)

デバッグ手法

  • Ripper.lexでトークン一覧を、Ripper.sexpでASTを出力できる
  • ruby -y test.rbで、パーサーの状態が遷移するたびに内部の状態をデバッグ出力できる
  • ruby --dump parsetree test.rbで、ASTノード構造体の情報を出力できる。

第2章 コンパイル

Rubyが実際に実行するコードは、元のコードと似ても似つかない

YARV

  • Ruby 1.9でYARV(Yet Another Ruby Virtual Machine)が導入された
  • YARVとJVMの違い
    • Rubyは独立したツールとしてコンパイラを提供するので花育、内部的に自動でコードをバイトコード命令列へとコンパイルする
    • Rubyはコードをバイトコード命令列へ翻訳するが、JVMのようにさらにバイトコード命令列のいくつかを機械語へコンパイルすることはしない
  • YARVを使う一番の理由は速さ
  • YARVはスタックマシン。YARV命令列用の引数と戻り値からなるスタックを保持する

コンパイル処理

  • Rubyコンパイラが実装されているMRIのCソースファイルはcompile.c
    • iseq_compile_each関数が現在のASTノードに対応するYARVコードを生成
    • NODE_CALL, NODE_FCALL, NODE_VCALLなどのノード表現ごとの大きなswitch文が存在

ローカルテーブル

  • 一度Rubyコンパイラが実行されると、ブロックパラメータに関する情報は、ローカルテーブルと呼ばれるASTの外の別のデータ構造にコピーされる
    • 各YARV命令列は専用のローカルテーブルを持つ
  • ローカルテーブルを表示させるには、RubyVM::InstructionSequence.compile(code).disasmでYARV命令列を表示させる

関数とメソッドの内部表現の違い

  • Rubyではすべての関数は、常にレシーバが存在するという意味で実際にはメソッド。しかし、Rubyの内部では、パーサとコンパイラは関数とメソッドを区別する。
    • メソッド:明確なレシーバを持つ
    • 関数:selfの現在の値をレシーバとみなす

第3章 Rubyはどのようにコードを実行するか

YARVはただのスタックマシンじゃない。2重スタックマシンなんだ!

YARVの管理する2種類のスタック

  • YARVは内部スタックとRubyプログラムのコールスタック、2重のスタックを管理する
  • rb_control_frame_t構造体に、Rubyのコールスタックが管理される
  • 主要レジスタ
    • SP(Stack Pointer): スタックの一番上を指す
    • PC:(Program Counter): 現在の命令の場所を指す
    • CFP(Control Frame Pointer): 現在の制御フレームを指す

YARV命令

  • 全てのYARV命令はCで実装されており、そのCコードが機械語にコンパイルされ、ハードウェアによって直接実行される
  • insns.defにYARV命令のCソースコードにコンパイルされる命令定義が記述されている(実際には、ビルド処理の中でinsns.defの内容がCソースコード vm.inc に変換される)

変数へのアクセス

  • sver/cref: YARVスタックにおいて、特殊変数のテーブルへのポインタ or 現在のレキシカルスコープへのポインタのいずれかを含む
    • sver: special variable
    • cref: class reference
  • EP(Environment Pointer): 環境ポインタ
    • 現在のメソッド用のローカル変数がスタック上のどこにあるかをポイントする
  • setlocal: ローカル変数の値を設定するYARV命令
    • Ruby 2.0からは、setlocal_OP__WC_0のような、最適化された命令が使われるように
  • getlocal: ローカル変数を取得するYARV命令
  • メソッド引数はローカル変数とみなされる
  • 異なるスコープで定義された変数を使用するのには動的変数アクセスが行われる

特殊変数リスト

  • Rubyがサポートする全ての特殊変数の正確なリストはparse.yファイルのparser_yylex関数に存在する

第4章 制御構造とメソッドディスパッチ

あなたがRubyで制御構造を使うように、YARVも内部で独自の制御構造を使う

制御構造

  • if文、if…else文やwhile…end/until…endなどの制御構造は、jump, branchunless&branchifで実装されている
  • break文、return文の実装では、捕捉テーブルを用いてYARV命令レベルでの例外送出ロジックを用いている
  • forループの内部実装はeachと同義。すなわち、for文はeach呼び出しの糖衣構文
    • RubyVM::InstructionSequence.compile(code).disasmにfor文を含むコード文字列を渡すと、eachを用いていることがわかる
      • 例:0004 send <callinfo!mid:each, argc:0>, <callcache>, block in <compiled> が見つかる

send命令

  • send命令はYARVに別のメソッドへジャンプし処理を開始することを伝える
  • 内部的にメソッドを11種類のタイプに分け、メソッドディスパッチの間、そのタインプに従ってそれぞれ異なる呼び出し方をする
    • ISEQ, CFUNC, ATTRSET, IVAR, BMETHOD, ZSUPER, UNDEF,NOTIMPLEMENTED, OPTIMIZED, MISSING, REFINED
    • このうち、以下は覚えておくと良さそう
      • ISEQ: Rubyを使ってユーザーが書いた通常のメソッド。intruction sequence
      • CFUNC: C function. Cコードで書かれRubyの実行ファイルに直接含まれるもの
      • ATTRSET: attribute set. attr_writerによって生成される
      • IVAR: instance variable. attr_readerによって生成される

第5章 オブジェクトとクラス

あらゆるRubyオブジェクトは、クラスへのポインタとインスタンス変数の配列が一体となったものである

NOTE: この章は特に書籍中の図表を見るのが一番理解が早い。自分でstructをみながら図を構築するのも理解の一助かと。

オブジェクト構造体

  • Rubyのオブジェクトの定義は、「すべてのRubyオブジェクトはクラスポインタとインスタンス変数の配列の組み合わせ」
  • RObject
    • RBasic
      • klass: クラスポインタ
      • ptr: rb_classext_structへのポインタ
    • numiv: 各オブジェクトが含むインスタンス変数の数
    • ivptr: 実際のインスタンス変数の配列へのポインタ

クラス構造体

  • Rubyのクラスの完全な定義は、「メソッド定義のグループと属性名のテーブル、スーパークラスポイント、定数テーブルも含んだRubyオブジェクト」
  • include/ruby/ruby.hで各構造体定義を確認できる
  • VALUEポインタを使って常にクラスにアクセス
  • RClass
    • m_tbl: メソッドテーブル
    • iv_index_tbl: 属性名テーブル
    • RBasic
      • klass: クラスポインタ
    • ptr: rb_classext_structへのポインタ
  • rb_classext_struct
    • super: スーパークラスへのポインタ
    • iv_tbl: クラスレベルのインスタンス変数
    • const_tbl: 定数テーブル
    • その他(origin, refined_class, allocator)

メタクラス

  • Rubyはクラスメソッドはメタクラスへと保存する
  • ObjectSpace.count_object[:T_CLASS]で、指定されたオブジェクト(ここではクラス)がどれくらい存在するかを確認できる
  • 新しいクラスを作成するたびに、内部的にはRubyは二つのクラスを作成する。2つ目のクラスはメタクラスで、FooClass.singleton_classでアクセス
    • #<Class:FooClass>構文が、それがFooClassのメタクラスであることがわかる