Rubiniusをhackしたことは特にないが、中を軽く読んでおこうという気持ちになったので読んだ内容をメモしておく。今日初めて読んだ状態なのであまり参考にならないかもしれない。
歴史
2007あたり開発開始
v1 (2010)
- 「Rubyで実装されたRuby」
- バイトコード仮想マシンはC++/LLVM
- LLVM JIT
- 世代別GC
- JRubyに次ぐ3番目にRailsが走ったRuby処理系
v2 (2013)
- 当時まだ出ていなかったRuby 2.1互換
- GILなし、マルチスレッドサポートの向上
- JITにより2-4倍程度まで高速化を達成
v3 (2016)
- ビルドにclang/clang++しかサポートしなくなった
- AOTコンパイルによるバイナリ生成もサポート
- 実行時にRubyを使わないようにした
- JITはバグが多かったため取り除かれた
特徴
- 2017/4/12時点ではRuby 2.3.1をターゲットにしている
- バイトコード仮想マシンはC++で実装されている
- 標準ライブラリはRubyで実装されている
- C拡張向けにC APIはある
- GILなしでネイティブスレッドを使える
- continuation, ripper, tracepoint, tracerなどの標準ライブラリをサポートしない
- refinementsと$SAFEもサポートしない
- Windowsをサポートしていない
実装の概要
Rubiniusのビルド
-
core rake taskでリポジトリ直下の
core/
ディレクトリに入っているRubyのコードを、同ファイル内のCodeDBCompiler.compile
がコンパイルする- 内部的には
Rubinius::ToolSets::Build::Compiler
を使ってコンパイルしている- このクラスは別リポジトリrubinius/rubinius-codeにgemがあってCRubyのgemとしてC++ extensionがビルドされ実行される。
- CRubyって書いてるけど既にrbxのバイナリがあればbootstrapはできるかもしれない
- パーサー、コンパイラはrubinius-codeの方のリポジトリを見る必要がある
- このクラスは別リポジトリrubinius/rubinius-codeにgemがあってCRubyのgemとしてC++ extensionがビルドされ実行される。
- 内部的には
rbxコマンド起動によるmain()の実行
machine/drivers/cli.cppにrbx(1)のmain()
がある
- Environnmentのコンストラクタでargc, argvを保存
- ここでrubinius::Stateも作られる。中にVMを保持しており、このStateインスタンスはいろんな場所で引き回される。
STATE
は#define STATE rubinius::State* state
。
- ここでrubinius::Stateも作られる。中にVMを保持しており、このStateインスタンスはいろんな場所で引き回される。
-
Environment#boot()を実行
- bootstrap_ontologyで最初に必要なクラス、メソッド、オブジェクトを用意
- load_argvでARG0, ARGVオブジェクトの読み込み
-
main用スレッドを起動。ここでJITとかも起動している
- Thread::main_threadがload_coreを呼び、
runtime/core
ディレクトリにあるファイルをこのへんでsignature, data, index, initializeなど読み込む- ここで呼ばれてるThread::createはこれ。
typedef Object* (*ThreadFunction)(STATE);
のため。 - 読み込む、というのは、Rubiniusのビルド時にruntime/core以下に吐いていたものは
rubinius::CompiledCode
(Rubiniusのバイトコード表現だと思われる)をシリアライズしたもの(ビルド後runtime/core/dataを見ればわかるが、バイナリではない)で、これをデシリアライズ後実行している。- その後
code->execute_script(state)
で実行するが、code->execute
の実態をどこでセットしているかはTODO - おそらくRubiniusのバイトコードをインタプリタ実行する
- その後
- ここで呼ばれてるThread::createはこれ。
-
Rubinius::Loaderをinstantiateして#mainを叩く。実行中はこの行より後には行かない。
-
Rubinius::Loader
を取得する際に呼ばれるget_constの実装。rubinius::Module
が再帰的にsuperclassを探索する。 -
.new
や#main
を呼ぶ時のObject::send
の実装。allow_primitiveはデフォルトtrue。- 内部でDispatch::sendを呼び出す。これがmethod_missingとかをハンドルしている
-
method->execute
で参照されているのはExecutableのexecuteフィールド。 - executor型は
typedef Object* (*executor)(State*, Executable* exec, Module* mod, Arguments& args);
なので、この型の関数を呼び出している
-
- Thread::main_threadがload_coreを呼び、
Rubinius::Loader#main
の実行
いろいろメソッドを呼び出すが、読んだところだけコメントする。
なお、このクラスはRubiniusのビルド時にCRuby + rubinius-code gemによってコンパイルされているので、このクラスの実行時点でRubinius自体がRuby言語のパーサやコンパイラを持っている必要はない
- preamble
- system_load_path
- signals
- run_compiled
-
ENV["RBX_RUN_COMPILED"]
が指定されてなければスキップされるのであまり気にしなくて良い - ARGVを全て
Rubinius.run_script
にかける。-
Rubinius.run_script
はRubinius.primitive :vm_run_script
を呼び出す。System::vm_run_script
が呼び出す実態はこれで、Rubiniusのバイトコードを実行するのではないか
-
-
- load_compiler
- こいつがコンパイル済のrubinius-code gemを読み込んでいる。ここで初めてRubiniusのランタイムにパーサ/コンパイラが出現する。
- rubinius-code gem自体をコンパイルするのをどこでやってるかは読んでないけど多分ビルド時にやるのではないか
-
Kernel.#require
の実態はRubinius::CodeLoader.require
であり、load_file
がRubinius::ToolSets::Runtime::Compiler
を使うので、load_compiler
をして初めてrequireできるようになる- 事実、これより前のメソッドはrequireをしていない
- こいつがコンパイル済のrubinius-code gemを読み込んでいる。ここで初めてRubiniusのランタイムにパーサ/コンパイラが出現する。
- preload
- detect_alias
- options
- 指定したスクリプトが
@script
に入る
- 指定したスクリプトが
- load_paths
- deprecations
- rubygems
- gemfile
-
-G
オプションでrequire 'bundler/setup'
するっぽい
-
- debugger
- profiler
- requires
-
-r
オプションの処理
-
- evals
-
-e
オプションの処理
-
- script
- optionsで処理した
@script
をRubyのスクリプトのファイル名として、CodeLoader.load_script
を呼び出す- 前述した
load_file
がここでも使われ、その後RubiniusのバイトコードをRubinius.run_script
する
- 前述した
- これがよく使う
ruby foo.rb
みたいなメインの処理と思われる
- optionsで処理した
- repl
まだ読めていないもの
- CompiledCodeや、core/dataからデシリアライズしたメソッドのexecuteをどこでセットしてるか
- Rubiniusのバイトコードインタプリタがどうやってコードを実行するのか
まとめ
RubiniusがRubyのスクリプトを読み込んで実行するまで:
- Rubiniusのビルド時に、ランタイム用にRuby言語で記述された
Rubinius::Loader
やRubinius::CodeLoader
をCRubyとrubinius-code gemでコンパイルし、Rubiniusのバイトコードインタプリタが理解できる状態にしておく - rbxコマンドが実行されると、argc, argvからARGVを作り、VMの準備後メインのスレッドが立ち上がり、そのスレッドでコンパイル済のバイトコードを実行することで
Rubinius::Loader#main
が呼ばれる -
Rubinius::Loader#main
は#load_compiler
を実行し、CRubyによってコンパイル済のrubinius-code gem(Rubinius用のRuby言語のパーサ、コンパイラ)を読み込む -
Rubinius::Loader#options
で読み込むスクリプトのファイル名をARGVから取得し、rubinius-code gemに入っているRubinius::ToolSets::Runtime::Compiler
がRubyをバイトコードに変換し、実行する