yomikomu(0.4.1)のコードリーディングをしました。
コードリーディングの前にRubyVM::InstructionSequence.load_iseqの説明をします。
Rubyでloadやrequireをすると
- RubyVM::InstructionSequence.load_iseqが定義されている場合、load_iseqを実行
- 戻り値がnilであればファイルから読み込んでパース・コンパイルして命令列に変換して実行
- 戻り値がnilでなければ、それを命令列として実行
- load_iseqが定義されていない場合は、ファイルを読みこんでパース・コンパイルして命令列に変換して実行
例えば以下のようなコードで
#!/usr/bin/env ruby
class RubyVM::InstructionSequence
def self.load_iseq(path)
puts '*****'
puts path
end
end
require 'csv'
これを実行すると、requireの度にRubyVM::InstructionSequence.load_iseqが呼ばれ、以下のように出力されます。
*****
/Users/mtajitsu/.rbenv/versions/2.5.0/lib/ruby/2.5.0/csv.rb
*****
/Users/mtajitsu/.rbenv/versions/2.5.0/lib/ruby/2.5.0/forwardable.rb
*****
/Users/mtajitsu/.rbenv/versions/2.5.0/lib/ruby/2.5.0/forwardable/impl.rb
*****
/Users/mtajitsu/.rbenv/versions/2.5.0/lib/ruby/2.5.0/English.rb
*****
/Users/mtajitsu/.rbenv/versions/2.5.0/lib/ruby/2.5.0/date.rb
上の例だとputsの戻り値=nilなので通常通りファイルがコンパイルされますが、ここでiseqを返してあげることでRubyに命令列を直接読み込ませることが可能です。
それではコードを読んでいきます。yomikomuはlib/yomikomu.rbのワンファイルになります。
環境変数が設定されている場合、RubyVM::InstructionSequence.load_iseqが呼ばれます。load_iseqはYomikomu::STORAGE.load_iseqを呼びます。
class RubyVM::InstructionSequence
if ENV['YOMIKOMU_YOMIKOMANAI'] != 'true'
def self.load_iseq fname
::Yomikomu::STORAGE.load_iseq(fname)
end
end
end
Yomikomu::STORAGEはストレージのクラスで環境変数によって変わります。
STORAGE = case storage = ENV['YOMIKOMU_STORAGE']
when 'fs'
FSStorage.new
when 'fsgz'
# ...
when nil
FSStorage.new
else
raise "Unknown storage type: #{storage}"
end
デフォルトはFSStrogareなので、以下FSStorageに関して見ていきます。
FSStorageはBasicStorageを継承しています。BasicStorage#load_iseqは以下のように定義されています。
class BasicStorage
def load_iseq fname
iseq_key = iseq_key_name(fname)
if compiled_iseq_exist?(fname, iseq_key) && compiled_iseq_is_younger?(fname, iseq_key)
::Yomikomu::STATISTICS[:loaded] += 1
::Yomikomu.debug{ "load #{fname} from #{iseq_key}" }
binary = read_compiled_iseq(fname, iseq_key)
iseq = RubyVM::InstructionSequence.load_from_binary(binary)
# p [extra_data(iseq.path), RubyVM::InstructionSequence.load_from_binary_extra_data(binary)]
# raise unless extra_data(iseq.path) == RubyVM::InstructionSequence.load_from_binary_extra_data(binary)
iseq
elsif YOMIKOMU_AUTO_COMPILE
compile_and_store_iseq(fname, iseq_key)
else
::Yomikomu::STATISTICS[:ignored] += 1
::Yomikomu.debug{ "ignored #{fname}" }
nil
end
end
コンパイルしたiseqが既に存在していて(compiled_iseq_exist?)、直近にコンパイルされたものであれば(compiled_iseq_is_younger?)、コンパイルされたiseqを読み込みRubyVM::InstructionSequenceのインスタンスを返します。そうでなければ、環境変数によって設定されるYOMIKOMU_AUTO_COMPILEがtrueであれば自動的にコンパイルを行い、YOMIKOMU_AUTO_COMPILEが設定されていなければnilを返し通常のファイル読み込み・パース・コンパイルを行います。
compiled_iseq_exist?やcompiled_iseq_is_younger?は以下のように定義されています。
class FSStorage < BasicStorage
def compiled_iseq_exist? fname, iseq_key
File.exist?(iseq_key)
end
def compiled_iseq_is_younger? fname, iseq_key
File.mtime(iseq_key) >= File.mtime(fname)
end
コンパイルしたファイルが存在し、コンパイル時よりもあとにファイルが更新されていない場合に読み込むようになっています。
read_compiled_iseqはFile.binreadでファイルの読み込みをしています。
def read_compiled_iseq fname, iseq_key
File.binread(iseq_key)
end