読者です 読者をやめる 読者になる 読者になる

たごもりすメモ

コードとかその他の話とか。

require_relativeはeval内で必ずLoadErrorになる on Ruby1.9.2

ruby

いま書いてるアプリをちゃんとrackupできるようにしよう、とあれこれやっていてハマったうちのひとつ。(他にも多数……というより、まだPassengerでちゃんと動いてない orz)

Sinatraアプリケーションをrackupするための config.ru を書くときに、世の中の解説を読むと以下のような内容になってる。

require 'app'
run Sinatra::Application

が、もちろんご存知の通り、ruby 1.9.2 では $LOAD_PATH に '.'(カレントディレクトリ)が含まれなくなり、requireで自前のアプリケーションを読み込むことはそのままでは不可能になっている。ので、じゃあどうすればいいのかというと以下のふたつ。

  • $LOAD_PATHにアプリケーションのディレクトリを加える
  • require_relativeを使う

$LOAD_PATH にアプリケーションのディレクトリを加えるのは正直よろしくないと思う。デプロイするマシンによって違うことも十分に考えられるし。
ということで普通は require_relative を使う。これはこの記述のあるファイルからの相対パスで読み込む先のファイルを指定するもので、アプリケーション内でお互いにファイルを読み込むには是非こっちを使うべきという用途のものだと言えよう。
ということで config.ru を以下のように変更した。

require_relative 'lib/yabitz/app'
run Sinatra::Application

すると動かない orz
rackup すると、エラーは以下のような感じ。泣ける。

dhcp79:yabitz tagomoris$ rackup
config.ru:2:in `require_relative': cannot infer basepath (LoadError)
	from config.ru:2:in `block in 
' from /usr/local/lib/ruby/gems/1.9.1/gems/rack-1.2.1/lib/rack/builder.rb:46:in `instance_eval' from /usr/local/lib/ruby/gems/1.9.1/gems/rack-1.2.1/lib/rack/builder.rb:46:in `initialize' from config.ru:1:in `new' from config.ru:1:in `
' from /usr/local/lib/ruby/gems/1.9.1/gems/rack-1.2.1/lib/rack/builder.rb:35:in `eval' from /usr/local/lib/ruby/gems/1.9.1/gems/rack-1.2.1/lib/rack/builder.rb:35:in `parse_file' from /usr/local/lib/ruby/gems/1.9.1/gems/rack-1.2.1/lib/rack/server.rb:162:in `app' from /usr/local/lib/ruby/gems/1.9.1/gems/rack-1.2.1/lib/rack/server.rb:248:in `wrapped_app' from /usr/local/lib/ruby/gems/1.9.1/gems/rack-1.2.1/lib/rack/server.rb:213:in `start' from /usr/local/lib/ruby/gems/1.9.1/gems/rack-1.2.1/lib/rack/server.rb:100:in `start' from /usr/local/lib/ruby/gems/1.9.1/gems/rack-1.2.1/bin/rackup:4:in `' from /usr/local/bin/rackup:19:in `load' from /usr/local/bin/rackup:19:in `
' dhcp79:yabitz tagomoris$

instance_eval とか見た瞬間に背筋を戦慄が走り抜ける。でもまあ .rb でもないファイルにrubyのコード書いてんだから、そりゃそうかな。

なお、このエントリの最後に config.ru をどうしたかが書いてあります。途中にはあんまり関係のないRuby実装のコードが大量にあるだけなので、config.ru をどうにかしたくてこのエントリにたどりついてしまった人はまっさきに末尾を読みましょう。

いちおうちゃんと調査してみる

あんまり当て推量で大声でモノを申してもハズれてたりすると恥ずかしいので、いちおう調べてみる。
とっかかりはLoadErrorの例外メッセージで、これでソースツリーをgrepする。と、以下のようなコードが出てくる。

/* load.c */
VALUE
rb_f_require_relative(VALUE obj, VALUE fname)
{
    VALUE rb_current_realfilepath(void);
    VALUE base = rb_current_realfilepath();
    if (NIL_P(base)) {
        rb_raise(rb_eLoadError, "cannot infer basepath");
    }
    base = rb_file_dirname(base);
    return rb_require_safe(rb_file_absolute_path(fname, base), rb_safe_level());
}

NIL_P() の定義はなんとなくわかるような気がするので置いておいて rb_current_realfilepath() がnilかゼロかNULLか、なんかそんなものを返す場合にマズいようだ。ということで次はそいつの定義を調べる。

/* vm_eval.c */
VALUE
rb_current_realfilepath(void)
{
    rb_thread_t *th = GET_THREAD();
    rb_control_frame_t *cfp = th->cfp;
    cfp = vm_get_ruby_level_caller_cfp(th, RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp));
    if (cfp != 0) return cfp->iseq->filepath;
    return Qnil;
}

ほうほう Qnil ってのがいかにもnilっぽいですね? これが返る条件は cfp == 0 の場合、と。でそれを返してくる vm_get_ruby_level_caller_cfp() の定義を眺める。

/* vm.c */
static rb_control_frame_t *
vm_get_ruby_level_caller_cfp(rb_thread_t *th, rb_control_frame_t *cfp)
{   
    if (RUBY_VM_NORMAL_ISEQ_P(cfp->iseq)) {
        return cfp;
    }   
    
    cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp);
    
    while (!RUBY_VM_CONTROL_FRAME_STACK_OVERFLOW_P(th, cfp)) {
        if (RUBY_VM_NORMAL_ISEQ_P(cfp->iseq)) {
            return cfp;
        }
        
        if ((cfp->flag & VM_FRAME_FLAG_PASSED) == 0) {
            break;
        }
        cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp);
    }
    return 0;
} 

最後に return 0; があるので、ここにたどりついてしまう条件を探せばいいんでしょう。見たところVMのスタックフレームを順々に上に登っていって RUBY_VM_NORMAL_ISEQ_P() がtrueになるものを探しているご様子。いちばん上まで着いちゃった場合?か、スタックオーバーフローしちゃった場合、あるいは break の入っている条件、つまり RUBY_VM_NORMAL_ISEQ_P(cfp->iseq) が 0 で、かつ cfp->flag の返値で VM_FRAME_FLAG_PASSED フラグが落ちていた場合に 0 が返る、と。
じゃあこの cfp 構造体とやらの中身 rb_control_frame_t の定義と使われかたを……と調べるところですが、実はそこまでやんなくても、ちょっと上にこんなことが書いてあったりする。

/* vm.c */
static void
vm_set_eval_stack(rb_thread_t * th, VALUE iseqval, const NODE *cref)
{
    rb_iseq_t *iseq;
    rb_block_t * const block = th->base_block;
    GetISeqPtr(iseqval, iseq); 
    
    /* for return */
    rb_vm_set_finish_env(th);
    vm_push_frame(th, iseq, VM_FRAME_MAGIC_EVAL, block->self,
                  GC_GUARDED_PTR(block->dfp), iseq->iseq_encoded,
                  th->cfp->sp, block->lfp, iseq->local_size);
                  
    if (cref) {
        th->cfp->dfp[-1] = (VALUE)cref;
    }   
    
    CHECK_STACK_OVERFLOW(th->cfp, iseq->stack_max);
} 

名前からして eval が呼ばれたときにスタックを積むための関数であることは疑わなくていいでしょう多分。問題のものは vm_push_frame() で、呼び出しの第3引数にセットされている VM_FRAME_MAGIC_EVAL があやしい。いちおう vm_push_frame() を見てみる。

/* vm_insnhelper.c */
static inline rb_control_frame_t *
vm_push_frame(rb_thread_t * th, const rb_iseq_t * iseq,
              VALUE type, VALUE self, VALUE specval,
              const VALUE *pc, VALUE *sp, VALUE *lfp,
              int local_size)
{   
    /* がばっと前略 */    
    /* setup vm control frame stack */
    
    cfp->pc = (VALUE *)pc;
    cfp->sp = sp + 1;
    cfp->bp = sp + 1;
    cfp->iseq = (rb_iseq_t *) iseq;
    cfp->flag = type;
    cfp->self = self;
    cfp->lfp = lfp;
    cfp->dfp = sp;
    cfp->proc = 0;
    cfp->me = 0;

    /* 後略 */

ということで第3引数に渡した値がそのまま cfp->flag で返ってくるらしいぞ。 VM_FRAME_MAGIC_EVAL の定義を見てみる。ついでに VM_FRAME_FLAG_PASSED も。両方とも vm_core.h にある。

/* vm_core.h */
#define VM_FRAME_MAGIC_METHOD 0x11
#define VM_FRAME_MAGIC_BLOCK  0x21
#define VM_FRAME_MAGIC_CLASS  0x31
#define VM_FRAME_MAGIC_TOP    0x41
#define VM_FRAME_MAGIC_FINISH 0x51
#define VM_FRAME_MAGIC_CFUNC  0x61
#define VM_FRAME_MAGIC_PROC   0x71
#define VM_FRAME_MAGIC_IFUNC  0x81
#define VM_FRAME_MAGIC_EVAL   0x91
#define VM_FRAME_MAGIC_LAMBDA 0xa1
#define VM_FRAME_MAGIC_MASK_BITS   8
#define VM_FRAME_MAGIC_MASK   (~(~0<<VM_FRAME_MAGIC_MASK_BITS))

#define VM_FRAME_TYPE(cfp) ((cfp)->flag & VM_FRAME_MAGIC_MASK)

/* other frame flag */
#define VM_FRAME_FLAG_PASSED 0x0100

#define RUBYVM_CFUNC_FRAME_P(cfp) \
  (VM_FRAME_TYPE(cfp) == VM_FRAME_MAGIC_CFUNC)

ん? これだと VM_FRAME_MAGIC_EVAL に限らず、どの種類のフレームでも VM_FRAME_FLAG_PASSED はそのままだと立ってねーじゃん? ということは別途 cfp->flag に対して VM_FRAME_FLAG_PASSED を足されたフレームだけが break する条件を満たす、ということになってしまう。おやあ。
じゃあこのフラグを立たせるコードは、と見ていくと……。

/* eval_intern.h */
#define PASS_PASSED_BLOCK_TH(th) do { \
    (th)->passed_block = GC_GUARDED_PTR_REF((rb_block_t *)(th)->cfp->lfp[0]); \
    (th)->cfp->flag |= VM_FRAME_FLAG_PASSED; \
} while (0)

#define PASS_PASSED_BLOCK() do { \
    rb_thread_t * const __th__ = GET_THREAD(); \
    PASS_PASSED_BLOCK_TH(__th__); \
} while (0)

PASS_PASSED_BLOCK() もしくはスレッドを指定して PASS_PASSED_BLOCK_TH(th) を呼ぶとこのフラグが現在のフレームに立つ。じゃあそれはどこから呼ばれてんだ。

/* eval.c */
void
rb_obj_call_init(VALUE obj, int argc, VALUE *argv)
{
    PASS_PASSED_BLOCK();
    rb_funcall2(obj, idInitialize, argc, argv);
}

/* vm_eval.c */
VALUE
rb_call_super(int argc, const VALUE *argv)
{
    PASS_PASSED_BLOCK();
    return vm_call_super(GET_THREAD(), argc, argv);
}
...
static VALUE
send_internal(int argc, const VALUE *argv, VALUE recv, call_type scope)
{
    VALUE vid;
    VALUE self = RUBY_VM_PREVIOUS_CONTROL_FRAME(GET_THREAD()->cfp)->self;
    rb_thread_t *th = GET_THREAD();

    if (argc == 0) {
        rb_raise(rb_eArgError, "no method name given");
    }

    vid = *argv++; argc--;
    PASS_PASSED_BLOCK_TH(th);

    return rb_call0(recv, rb_to_id(vid), argc, argv, scope, self);
}

これくらい*1。なんとなく内部的に別の呼び出しに飛んでいく場合……のように見えるが、気になるのは rb_obj_call_init() で、これって要するに eval が呼ばれてからスタックに積まれてる

の初期化処理のことなんじゃねーの? と。

結論:あんまりちゃんと調査できなかった orz

結局推測で終わりかよ、という感じですが、まあこんな感じで……。
require_relativeは元になるソースコードファイルがあっての命令なのは確かだと思うんでeval内で使えませんよ、と言われても違和感は特にない。ただし本当に想定内の動作だったのかなというと疑問が残る。
まあ全体でのトップレベルの初期化処理(スタックフレームの底)以外にも初期化フレームがあるとすればevalを呼んだ境界線で、そこを超える瞬間にはどこのファイルがbasepathになるのと言われるとわかんないよね、と考えてこういうコードになったとすれば、その通りかなあ。でもそれだったら最初から VM_FRAME_MAGIC_EVAL と一致するかどうか調べればいいんじゃね? このコードだと super や send があったときにも何かが起きるよな?

と書いてからさらに気付いたけど、そういえば RUBY_VM_NORMAL_ISEQ_P(cfp->iseq) が真になるならそもそもフレームのフラグとか関係ないよね、と思ったが、そっちは追っていくと内部仕様にどっぷりとハマるよくわからないコード塊が出てきて、そこで追うのを諦めてしまった。
普通のメソッド呼び出しとかで積まれたスタックフレームの場合はそこが問題なく真になってるんだろうから、むしろ本質的にはそっちが問題のような気もする。しかし ISEQ とか言われても意味わかんないしー。いま調べたらInstractionSequenceの略っぽい。ほんとかな? こんなライブラリもある。
てことは通常のYARV命令なのかそうでないものなのか(YARVの「通常でない命令」ってなんだ?)、によって違うのか。へー。もう完全に内部仕様の話になっちゃってるからなあ……。

で config.ru をどうするか

すっかり忘れていたが、本題はうまく rackup するために config.ru にはどう書くべきか、という話であった。
require_relative は以上の調査によりなんかダメっぽいので、諦めて $LOAD_PATH を追加することにする。

# config.ru
$LOAD_PATH.push('/Users/tagomoris/Documents/yabitz')
require 'lib/yabitz/app'
run Sinatra::Application

えー、結局こんなオチかよー。(どんびき

*1:proc.cにもあった。ざっと見た感じProc内部から例外で脱出してきたときの処理っぽかったが、省略。