2018-03-26

yomikomuコードリーディング

yomikomu(0.4.1)のコードリーディングをしました。

コードリーディングの前にRubyVM::InstructionSequence.load_iseqの説明をします。

Rubyでloadやrequireをすると

  1. RubyVM::InstructionSequence.load_iseqが定義されている場合、load_iseqを実行
    • 戻り値がnilであればファイルから読み込んでパース・コンパイルして命令列に変換して実行
    • 戻り値がnilでなければ、それを命令列として実行
  2. 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

参考

このエントリーをはてなブックマークに追加