yarv2llvmの拡張のドキュメントをここに書く

AOベンチの高速化は結局手詰まりになってしまいました。今は、数ヶ月ほったらかしにしていたThreadのランタイムを再開しています。ジャイアントロックの無いThreadのランタイムと排他制御処理の自動挿入を実現してCPUをこき使えるといいなと思っています。
Thread周りのランタイムは結構低レベルの処理が満載でRubyではなかなか書けそうもないです。そこで、節操無く言語仕様を拡張して無理やり書こうとしています。この言語仕様の拡張はどこにもドキュメントが(ソースを除いて)無いので、多分すぐ忘れてしまうと思います。下手すると、ドキュメントを書いても書いたことを忘れてしまうので、Google先生には覚えておいてもらおうという魂胆です。

Threadのランタイム(runtime/thread.rb)はこんな感じのコードです。

#
# Runtime Library
#   Thread class --- for create native-thread
#
#   This file uses Unsafe objects
#
module LLVM::Runtime

  YARV2LLVM::define_macro :define_thread_structs do |system|
    
    if system[0][0].type.constant == :WIN32 then
      native_thread_t = "VALUE"
      rb_thread_lock_t = "VALUE"
    else
      native_thread_t = "P_VALUE, VALUE"
      rb_thread_lock_t = "VALUE"
    end

<<`EOS`
  VALUE = RubyHelpers::VALUE
  LONG  = LLVM::Type::Int32Ty
  VOID  = LLVM::Type::VoidTy
  P_VALUE = LLVM::pointer(VALUE)

中略

  RB_THREAD_T = LLVM::struct [
   VALUE,               # self
   RB_VM_T,                     # VM

中略

   [LONG, :state],                        # state

   RB_BLOCK_T,                  # passed_block
   
   VALUE,                       # top_self
   VALUE,                       # top_wrapper

中略

   #{native_thread_t},          # native_thread_data
   P_VALUE,                     # blocking_region_buffer

   VALUE,                       # thgroup
   VALUE,                       # value

   VALUE,                       # errinfo
   VALUE,                       # thrown_errinfo
   LONG,                        # exec_signal

   LONG,                        # interrupt_flag
   #{rb_thread_lock_t},         # interrupt_lock
   VALUE,                       # unblock_func
   VALUE,                       # unblock_arg
   VALUE,                       # locking_mutex

中略
   
  ]
EOS
  end

  define_thread_structs(:LINUX)

  def y2l_create_thread
    type = LLVM::function(VALUE, [VALUE])
    YARV2LLVM::LLVMLIB::define_external_function(:rb_thread_alloc, 
                                                 'rb_thread_alloc', 
                                                 type)
    thval = rb_thread_alloc(Thread)
    thval2 = YARV2LLVM::LLVMLIB::unsafe(thval, RDATA)
    th = YARV2LLVM::LLVMLIB::unsafe(thval2[4], RB_THREAD_T)
    st = th.address_of :state
    st0 = th[:state]
    st[0]
    thval
  end
end


ThreadのランタイムはOS等によって異なる構造体等を用いる必要があります。つまり、条件コンパイルを行わないといけないのですが、これをマクロによって行います。

  YARV2LLVM::define_macro :define_thread_structs do |system|

引数(system)にアーキテクチャを入れます。RUBY_PLATFORMあたりがいいのかなと思います。今回は適当です。

    if system[0][0].type.constant == :WIN32 then
      native_thread_t = "VALUE"
      rb_thread_lock_t = "VALUE"
    else
      native_thread_t = "P_VALUE, VALUE"
      rb_thread_lock_t = "VALUE"
    end

systemはいろんなメタ情報を含めて渡ってきますので、値を取り出すには、system[0][0].type.constantとします。この辺はあんまりな仕様なので変わるかもしれない、というかいい案があれば変えます。
アーキテクチャによって構造体を変えたり出来ます。プログラムは文字列で記述しておきます。
その後、テンプレート中に#{native_thread_t}という形で埋め込みます。

<<`EOS`
  VALUE = RubyHelpers::VALUE
  LONG  = LLVM::Type::Int32Ty
  VOID  = LLVM::Type::VoidTy
  P_VALUE = LLVM::pointer(VALUE)

  RB_THREAD_T = LLVM::struct [
中略

   #{native_thread_t},          # native_thread_data
   P_VALUE,                     # blocking_region_buffer

yarv2llvmで構造体を定義するには、LLVM::structにメンバーの配列を渡します。メンバーはLLVMの型オブジェクトか、[型オブジェクト, シンボル]の配列で、配列で渡した場合、ディリファレンス時にシンボルでメンバーを指定できます。実装を簡単にするためLLVM::structが実際に生成するのは、構造体へのポインタになります。
また、LLVM::pointerを使うと、ポインターを定義できます。

ランタイムを書くには、Cの関数を呼んだりCの構造体を扱ったりする必要があります。

Cの関数を呼ぶには

    type = LLVM::function(VALUE, [VALUE])
    YARV2LLVM::LLVMLIB::define_external_function(:rb_thread_alloc, 
                                                 'rb_thread_alloc', 
                                                 type)
    thval = rb_thread_alloc(Thread)

とします。ここで、YARV2LLVM::LLVMLIB::define_external_functionという長いメソッドがキーになります。これは、次のような3つの引数をとります。

  • yarv2llvmのメソッドの名前
  • Cの関数名
  • 関数の型

こうすると、あたかもCの関数をyarv2llvmのメソッドのように扱うことができます。
この場合はVALUE型というRubyレベルで扱えるデータが返ってくるのでいいのですが、関数によってはGCがコアを吐いちゃうような値を返す場合があります。ランタイムを作るにはこのような関数も呼ばなければなりません。
そのような目的の為、Unsafeというオブジェクトを用意しています。UnsafeオブジェクトはCのポインタや構造体をWrapしたもので、メソッドしてデリファレンスと代入しか用意していません。静的に他のオブジェクトと区別できますので、インスタンス変数とかGCがコアを吐いちゃう恐れのあるところには入らないようにコンパイル時にチェックします。

    thval = rb_thread_alloc(Thread)

    thval2 = YARV2LLVM::LLVMLIB::unsafe(thval, RDATA)
    th = YARV2LLVM::LLVMLIB::unsafe(thval2[4], RB_THREAD_T)
    st = th.address_of :state
    st0 = th[:state]
    st[0]
    thval

これで、thval2にはthvalの値でRDATA構造体を型としたUnsafeオブジェクトが入ります。例えば、@foo = thval2とかやると、エラーが起きます。YARV2LLVM::LLVMLIB::unsafeはちょうどCのキャスト演算子みたいな動きをします。

Unsafeオブジェクトは次の3つのメソッドを持ちます

  • [](番号またはシンボル) 構造体ならばメンバーの値を返す。ポインタならばオフセットをつけてディリファレンスする。
  • =(番号またはシンボル, 値) の値を書き換える
  • address_of(番号またはシンボル) 構造体の場合、メンバーのアドレスを返す

これらの結果は、Unsafeオブジェクトですが結果をYARV2LLVM::LLVMLIB::unsafeをつかってキャストすることもできます。また、この結果がVALUE型で安全という保障があれば、YARV2LLVM::LLVMLIB::safeメソッドを使ってインスタンス変数等に代入できる普通のオブジェクトに変換することもできます。

いずれにしても、どれもこれも非常に危ないので失敗すると即コア吐きます。コンパイラの挙動も怪しいので生成されたLLVM IRとにらめっこして正しいコードかチェックする必要があります。