vim の :rubyfile は rb_load を使うが ruby の load は rb_f_load を使う

事の発端

if_ruby を使って何かしたいなぁということで :rubyfile test.rb を実行したが、LoadError: cannot load such file -- test.rb となって実行できなかった。ただ、:ruby load "test.rb" は実行できるので、何が違うのか?という疑問が事の発端

vim-jp の slack で質問

slack で質問してみると :rubyfilerubyの公開APIrb_load() を呼んでいるだけとのこと。また、ruby loadKernel.#load‘ を使用しているが、なぜ:rubyfile` のほうが動かないのかはわからないとのことで原因を追究してみたくなった。

f:id:bamch0h:20190620222035p:plain

Kernel.#load は rb_load を実行しているのか?

2.6のブランチの load.c を見ると、 loadrb_f_load() をコールしているようです。

https://github.com/ruby/ruby/blob/ruby_2_6/load.c#L1233

rb_loadload.cに実装があり、単に rb_load_internal() をコールしているのみとなっているようです。

https://github.com/ruby/ruby/blob/ruby_2_6/load.c#L659

rb_f_load() の中でも rb_load_internal() をコールしていますが、前処理がいくつか入っているため、その部分に今回の原因がありそう?

rb_f_load の前処理を追う

追うといっても前処理で生成されている文字列を一個一個printfで出力してみるというだけのことをしました。

static VALUE
rb_f_load(int argc, VALUE *argv)
{
    printf("rb_f_load start\n"); // 追加
    VALUE fname, wrap, path, orig_fname;

    rb_scan_args(argc, argv, "11", &fname, &wrap);

    printf("fname(1):  %s\n", StringValueCStr(fname)); // 追加

    file_to_load(fname);

    orig_fname = rb_get_path_check_to_string(fname, rb_safe_level());
    printf("orig_fname:%s\n", StringValueCStr(orig_fname)); // 追加
    fname = rb_str_encode_ospath(orig_fname);
    printf("fname(2):  %s\n", StringValueCStr(fname)); // 追加
    RUBY_DTRACE_HOOK(LOAD_ENTRY, RSTRING_PTR(orig_fname));

    path = rb_find_file(fname);
    if (!path) {
        printf("path is null?? \n"); // 追加
        if (!rb_file_load_ok(RSTRING_PTR(fname)))
            load_failed(orig_fname);
        path = fname;
    }
    printf("path:      %s\n", StringValueCStr(path)); // 追加
    printf("wrap:      %d\n", RTEST(wrap)); // 追加
    rb_load_internal(path, RTEST(wrap));

    RUBY_DTRACE_HOOK(LOAD_RETURN, RSTRING_PTR(orig_fname));

    printf("rb_f_load start\n"); // 追加

    return Qtrue;
}

結果的には、load "foo.rb" のようなスクリプトで文字列が変化することはなく。rb_load_internal() には foo.rb という文字列がそのまま渡されているようでした。wrap の値が怪しいのかな?とも思って見てみましたが、それも if_ruby の値と同じでした。

rb_load の実装を見てみる

rb_load()rb_load_internal()をコールしているだけといいましたが、実際には引数で渡ってきたパスを一旦file_to_load()関数に渡していました。

void
rb_load(VALUE fname, int wrap)
{
    rb_load_internal(file_to_load(fname), wrap);
}

file_to_load() の実装は以下の通りです。FilePathValue() ではエラーになってなかったっぽいので、rb_find_file()でエラーになって返り値が0になるため、load_failed()が呼ばれてしまうようです。

static VALUE
file_to_load(VALUE fname)
{
    VALUE tmp = rb_find_file(FilePathValue(fname));
    if (!tmp) load_failed(fname);
    return tmp;
}

rb_find_filerb_f_load() の中でも使われており、返り値が0の場合の処理が rb_load()とは異なっているようです。

    path = rb_find_file(fname);
    if (!path) {
        if (!rb_file_load_ok(RSTRING_PTR(fname)))
            load_failed(orig_fname);
        path = fname;
    }

rb_file_load_ok() という関数がキモな気がしてきました。

rb_file_load_ok()file.c 内に定義されています。ただ、内容を見た感じ、fdを取得して内部でロード可能かを判断しているだけのようなので、特別何があるというわけでもなさそう。カレントディレクトリにロード対象のファイルがあれば、それをちゃんとロード可能だと判断してくれそうな気がします。というかしてくれます。

結論

Kernel.#loadrb_f_load()を呼んでいて、vim:rubyfilerb_load()を呼んでいる。rb_load() の中では file_to_load()が使われていて、この関数の中のrb_find_file()が返り値0を返すためロードエラーとなっている。rb_f_load() でも同関数が使われている。ただ、一度ロードには失敗するが、そのあとの処理でrb_file_load_ok() でロード可能と判断されるため正常にロードされる。

なぜ、このような違いがあるのかまでは今回は追えなかったが、一応 :rubyfileruby load と異なる動作をする理由がわかったので今回は終了とする。