第18章 ロード

概要

インターフェイス

Rubyレベルでロードに使える手続きは二つある。 requireloadだ。

require 'uri'            # URIのライブラリをロード
load '/home/foo/.myrc'   # なにかのリソースファイルを読む

どちらも通常のメソッドであり、他のコードと全く同様にコンパイル され、評価される。つまり、完全にコンパイルして評価の段階に移ってから ロードが起こる。

二つのインターフェイスははっきりと用途が分離されている。 ライブラリをロードするときはrequire、任意のファイルをロード しようとするときはloadだ。それぞれ詳しく特徴を説明していこう。

require

requireの特徴は四点だ。

Rubyのロードパスは$:というグローバル変数に入っていて、その値は 文字列配列である。例えば筆者が普段使っている環境で$:の中身を表示して みたらこうなった。

% ruby -e 'puts $:'
/usr/lib/ruby/site_ruby/1.7
/usr/lib/ruby/site_ruby/1.7/i686-linux
/usr/lib/ruby/site_ruby
/usr/lib/ruby/1.7
/usr/lib/ruby/1.7/i686-linux
.

配列はputsするだけで一要素が一行に表示されるのでとても見やすい。

筆者は--prefix=/usrconfigureしている のでライブラリのパスが/usr/lib/ruby以下になっているが、普通にソース コードからコンパイルした場合は/usr/local/lib/ruby以下に入る。 Windows環境ならさらにドライブレターも付く。

さて、このロードパスから標準ライブラリのnkf.sorequireしてみよう。

require 'nkf'

requireした名前(require名と呼ぼう)に拡張子が付いていないと、 requireは勝手に拡張子を補うようになっている。まず.rbを試し、次に.soを 試す。もちろん拡張ライブラリの拡張子はプラットフォームごとに独立で、 Windows環境なら.dllも試すし、Mac OS Xなら.bundleを試す。

筆者の環境を使ってシミュレーションしてみよう。 rubyは次のようなパスを順番に確かめてみる。

/usr/lib/ruby/site_ruby/1.7/nkf.rb
/usr/lib/ruby/site_ruby/1.7/nkf.so
/usr/lib/ruby/site_ruby/1.7/i686-linux/nkf.rb
/usr/lib/ruby/site_ruby/1.7/i686-linux/nkf.so
/usr/lib/ruby/site_ruby/nkf.rb
/usr/lib/ruby/site_ruby/nkf.so
/usr/lib/ruby/1.7/nkf.rb
/usr/lib/ruby/1.7/nkf.so
/usr/lib/ruby/1.7/i686-linux/nkf.rb
/usr/lib/ruby/1.7/i686-linux/nkf.so    発見!

/usr/lib/ruby/1.7/i686-linuxnkf.soが見付かった。見付かったら、 requireの最後の特徴……同じファイルを二度ロードしない……ためにロック をかける。ロックは$"というグローバル変数に入っている文字列配列で、今回 は"nkf.so"という文字列が入る。たとえrequireするときに拡張子を省略して あったとしても、$"に入れるときにはそれを補ったファイル名になる。

require 'nkf'   # nkfをロードすると……
p $"            # ["nkf.so"] ロックされる。

require 'nkf'   # もう一回requireしても何も起こらない。
p $"            # ["nkf.so"] ロック配列の内容は変わらない。

$"で拡張子が補われる理由は二つある。一つは後から同じファイルを拡張子付 きでrequireされたときに二度ロードしないようにすること、もう一つは nkf.rbnkf.soを両方ロードできるようにすることだ。また実際の拡張子はプ ラットフォームごとに.so .dll .bundleなどとバラバラだったが、ロックする ときは常に.soになる。だからRubyプログラムを書いている間は拡張子の違い は無視して常に.soを仮定してよい。ここで.soになるあたりがrubyが UNIX指向と言われる所以だ。

ちなみに$"はRubyレベルからでも好きなようにいじれてしまうので強固なロッ クとは言えない。例えば$"をクリアすれば同じ拡張ライブラリを何回もロード できてしまう。

load

loadのほうはrequireと比べるとずっと簡単だ。requireと同じくファイルを $:から探す。ロードできるのはRubyプログラムだけ。また拡張子は省略できず、 常に完全なファイル名を指定しないといけない。

load 'uri.rb'   # 標準添付のURIライブラリをロード

ここでは簡単な例としてライブラリをロードしてみたが、フルパスを指定して リソースファイルをロードしたりするのがloadのまっとうな使いかたである。

処理全体の流れ

おもいきり大雑把に分けると「ファイルをロードする」という操作は

の三段階に分かれている。requireloadで違うのはファイルを見付ける 手順だけだ。残りはrequireだろうとloadだろうと違いはない。

最後の評価の段階についてもう少しだけ説明しておく。Rubyプログラムの場合、 ロードされたプログラムは基本的にはトップレベルで評価される。つまり定数 を定義すればトップレベルの定数となるし、メソッドを定義すれば関数風メソッ ドになる。

### mylib.rb
MY_OBJECT = Object.new
def my_p(obj)
  p obj
end

### first.rb
require 'mylib'
my_p MY_OBJECT   # 別のファイルで定義した定数やメソッドも使える

ただしトップレベルのローカル変数スコープだけはファイルが変わると別に なる。つまり別のファイル間ではローカル変数を共有することはできない。も ちろんProcその他を駆使すれば共有自体はできるが、それはロードのメカニズ ムを使って共有しているのではない。

それと、ときどき勘違いする人がいるのだが、どこでロードしてもクラスがロー ドされる先が変わったりはしない。例えば次のようにmodule文の中でロードし ても何の意味もなく、ファイルのトップレベルにあるものは全てトップレベル に置かれる。

require 'mylib'     # トップレベルでrequireしても、
module SandBox
  require 'mylib'   # モジュールの中でrequireしても、結果は同じ。
end

本章の見どころ

以上を踏まえてこれから読んでいくわけだが、今回は仕様がかなり詳細に決まっ ているのでただただ読んでみてもコードの羅列になりかねない。そこで本章で は以下の三点にターゲットを絞り込む。

第一点は現物を見ればわかる。

第二点に関して。本章の関数はeval.c ruby.c file.c dln.cの四つのファイ ルから入り乱れて登場する。どうしてそうなってしまうのか、そのあたりの現 実的な事情を考えてみようと思う。

第三点は読んでの通り。最近流行りの実行時ロード、俗に言うプラグインの仕組 みを見ていく。ここは本章で一番面白いところなので、ページもできるだけた くさん割いて話していきたい。

ライブラリの探索

rb_f_require()

requireの実体はrb_f_require()である。まずはその中からファイル探索の 部分だけを載せよう。場合分けが多いと嫌なので、引数は拡張子なしで 指定された場合に限定する。

rb_f_require()(簡約版)

5527  VALUE
5528  rb_f_require(obj, fname)
5529      VALUE obj, fname;
5530  {
5531      VALUE feature, tmp;
5532      char *ext, *ftptr; /* OK */
5533      int state;
5534      volatile int safe = ruby_safe_level;
5535
5536      SafeStringValue(fname);
5537      ext = strrchr(RSTRING(fname)->ptr, '.');
5538      if (ext) {
              /* ……拡張子が指定された場合…… */
5584      }
5585      tmp = fname;
5586      switch (rb_find_file_ext(&tmp, loadable_ext)) {
5587        case 0:
5588          break;
5589
5590        case 1:
5591          feature = fname = tmp;
5592          goto load_rb;
5593
5594        default:
5595          feature = tmp;
5596          fname = rb_find_file(tmp);
5597          goto load_dyna;
5598      }
5599      if (rb_feature_p(RSTRING(fname)->ptr, Qfalse))
5600          return Qfalse;
5601      rb_raise(rb_eLoadError, "No such file to load -- %s",
                   RSTRING(fname)->ptr);
5602
5603    load_dyna:
          /* ……拡張ライブラリをロード…… */
5623      return Qtrue;
5624
5625    load_rb:
          /* ……Rubyプログラムをロード…… */
5648      return Qtrue;
5649  }

5491  static const char *const loadable_ext[] = {
5492      ".rb", DLEXT,    /* DLEXT=".so", ".dll", ".bundle"... */
5493  #ifdef DLEXT2
5494      DLEXT2,          /* DLEXT2=".dll" on Cygwin, MinGW */
5495  #endif
5496      0
5497  };

(eval.c)

この関数ではgotoラベルのload_rbload_dyna以降が事実上のサブルーチン のようになっており、二つの変数featurefnameがその引数のような存在 である。その変数は次のような意味を持つ。

変数意味
feature$"に入れる形式のライブラリ名uri.rbnkf.so
fnameライブラリのフルパス/usr/lib/ruby/1.7/uri.rb

「feature」という語はどうやら一般名詞らしく、 rb_feature_p()という呼び出しが見える。これは$"のロックが かかっているかチェックする関数である(すぐ後で見る)。

実際にライブラリを探しているのはrb_find_file()rb_find_file_ext()で ある。rb_find_file()はロードパス$:からファイルを探してくる。 rb_find_file_ext()も同じだが、第二引数に拡張子のリスト(つまり loadable_ext)をもらいそれを順番に付けて試す、というところが違う。

以下ではとりあえず探索のコードを最後まで見て、そのあとload_rbrequireロックのコードを見ることにしよう。

rb_find_file()

まず探索の続きでrb_find_file()だ。この関数は引数のファイルpathを グローバルなロードパス$:rb_load_path)から探す。汚染文字列チェック だのなんだのがうるさいので主要部分だけにして見てみる。

rb_find_file()(簡約版)

2494  VALUE
2495  rb_find_file(path)
2496      VALUE path;
2497  {
2498      VALUE tmp;
2499      char *f = RSTRING(path)->ptr;
2500      char *lpath;

2530      if (rb_load_path) {
2531          long i;
2532
2533          Check_Type(rb_load_path, T_ARRAY);
2534          tmp = rb_ary_new();
2535          for (i=0;i<RARRAY(rb_load_path)->len;i++) {
2536              VALUE str = RARRAY(rb_load_path)->ptr[i];
2537              SafeStringValue(str);
2538              if (RSTRING(str)->len > 0) {
2539                  rb_ary_push(tmp, str);
2540              }
2541          }
2542          tmp = rb_ary_join(tmp, rb_str_new2(PATH_SEP));
2543          if (RSTRING(tmp)->len == 0) {
2544              lpath = 0;
2545          }
2546          else {
2547              lpath = RSTRING(tmp)->ptr;
2551          }
2552      }

2560      f = dln_find_file(f, lpath);
2561      if (file_load_ok(f)) {
2562          return rb_str_new2(f);
2563      }
2564      return 0;
2565  }

(file.c)

やっていることをRubyで書くとこうなる。

tmp = []                     # 配列を作る
$:.each do |path|            # ロードパスに対して繰り返し
  tmp.push check(path)       # パスをチェックしつつ配列にプッシュ
end
lpath = tmp.join(PATH_SEP)   # PATH_SEPを要素間に狭んで連結

dln_find_file(f, lpath)      # 本処理

PATH_SEPpath separator、つまりUNIXでは':'、Windowsでは';'で ある。rb_ary_join()はこの文字を要素の間に狭んだ文字列を作る。つまり せっかく配列になっているロードパスを文字区切りの文字列に戻しているので ある。

なんでこんなことをするのだろう。それは、dln_find_file()が受け付ける のがPATH_SEP区切り文字列だけだからだ。ではなぜdln_find_file()がそ ういう実装にならざるを得ないかというと、dln.crubyのライブラリで はない、ということになっているからだ。同じ作者によって書かれてはいても これは汎用ライブラリなのである。だからこそ序章『導入』でソース ファイルの分類をしたときも「ユーティリティ」に区分されていた。汎用ライ ブラリであるということはRubyオブジェクトを渡したりできないしrubyのグ ローバル変数を参照させるわけにもいかないのである。

またdln_find_file()の中では~をホームディレクトリに展開していたりす るのだが、実はこれもrb_find_file()の省略した部分で既にやっているので rubyのことだけ考えるなら必要ない。

ロードウェイト

ファイル探索はこのへんであっさり終わって、次はロードのコードだ。 より正確には「ロードの直前まで」である。 rb_f_require()load_rbのコードを以下に載せる。

rb_f_require():load_rb

5625    load_rb:
5626      if (rb_feature_p(RSTRING(feature)->ptr, Qtrue))
5627          return Qfalse;
5628      ruby_safe_level = 0;
5629      rb_provide_feature(feature);
5630      /* Rubyプログラムのロード作業はシリアライズする */
5631      if (!loading_tbl) {
5632          loading_tbl = st_init_strtable();
5633      }
5634      /* partial state */
5635      ftptr = ruby_strdup(RSTRING(feature)->ptr);
5636      st_insert(loading_tbl, ftptr, curr_thread);
          /* ……Rubyプログラムをロードして評価…… */
5643      st_delete(loading_tbl, &ftptr, 0); /* loading done */
5644      free(ftptr);
5645      ruby_safe_level = safe;

(eval.c)

前述の通りrb_feature_p()$"のロックがかかっているかチェックする。 そしてrb_provide_feature()$"に文字列をプッシュする。つまり ロックする。

問題はその次のところだ。コメントにある通り「Rubyプログラムのロードはシ リアライズされる」。つまり一つのファイルは一つのスレッドでしかロードで きず、ロード中に他のスレッドから同じファイルをロードしようとすると前 のロードが完了するまで待たされる。そうでないと、

Thread.fork {
    require 'foo'   # require開始時点でfoo.rbが$"に追加される、
}                   # しかしfoo.rbを評価中にスレッドが変わる
require 'foo'   # $"にfoo.rbが入っているのですぐ戻る
# (A)fooのクラスを使う……

なんてことをすると、本当はまだライブラリfooがロードされきって いないのに(A)のコードを実行されてしまうことがある。

待ちを入れる仕組みは簡単である。グローバル変数loading_tblst_tableを 作り、「feature=>ロードするスレッド」の対応を記録しておく。curr_threadeval.cの変数で、値は現在実行中のスレッドである。これが排他ロックに なってなっているわけだ。そしてrb_feature_p()の中で次のように、ロード中の スレッドがロードを完了するまで待つ。

rb_feature_p()(後半)

5477  rb_thread_t th;
5478
5479  while (st_lookup(loading_tbl, f, &th)) {
5480      if (th == curr_thread) {
5481          return Qtrue;
5482      }
5483      CHECK_INTS;
5484      rb_thread_schedule();
5485  }

(eval.c)

rb_thread_schedule()を呼ぶとその中で別のスレッドに制御が移り、 自分に制御が戻ると関数から返ってくる。loading_tblからファイル名が なくなればロード終了なので終わってよい。curr_threadのチェックをして いるのは自分自身をロックしないようにするためである(図1)。

(loadwait)
図1: ロードのシリアライズ

Rubyプログラムのロード

rb_load()

ではロードの本処理部分を見ていく。rb_f_require()load_rbのうち、 Rubyプログラムをロードする部分から始めよう。

rb_f_require()−load_rb−ロード

5638      PUSH_TAG(PROT_NONE);
5639      if ((state = EXEC_TAG()) == 0) {
5640          rb_load(fname, 0);
5641      }
5642      POP_TAG();

(eval.c)

さてここで呼んでいるrb_load()、これは実はRubyレベルのloadの実体である。 ということは探索がもう一回必要になるわけで、同じ作業をもう一回見るなん てやっていられない。そこでその部分は以下では省略してある。 また第二引数のwrapも、上記の呼び出しコードで0なので、0で畳み込んである。

rb_load()(簡約版)

void
rb_load(fname, /* wrap=0 */)
    VALUE fname;
{
    int state;
    volatile ID last_func;
    volatile VALUE wrapper = 0;
    volatile VALUE self = ruby_top_self;
    NODE *saved_cref = ruby_cref;

    PUSH_VARS();
    PUSH_CLASS();
    ruby_class = rb_cObject;
    ruby_cref = top_cref;           /*(A-1)CREFを変える */
    wrapper = ruby_wrapper;
    ruby_wrapper = 0;
    PUSH_FRAME();
    ruby_frame->last_func = 0;
    ruby_frame->last_class = 0;
    ruby_frame->self = self;        /*(A-2)ruby_frame->cbaseを変える */
    ruby_frame->cbase = (VALUE)rb_node_newnode(NODE_CREF,ruby_class,0,0);
    PUSH_SCOPE();
    /* トップレベルの可視性はデフォルトでprivate */
    SCOPE_SET(SCOPE_PRIVATE);
    PUSH_TAG(PROT_NONE);
    ruby_errinfo = Qnil;  /* 確実にnilにする */
    state = EXEC_TAG();
    last_func = ruby_frame->last_func;
    if (state == 0) {
        NODE *node;

        /* (B)なぜかevalと同じ扱い */
        ruby_in_eval++;
        rb_load_file(RSTRING(fname)->ptr);
        ruby_in_eval--;
        node = ruby_eval_tree;
        if (ruby_nerrs == 0) {   /* パースエラーは起きなかった */
            eval_node(self, node);
        }
    }
    ruby_frame->last_func = last_func;
    POP_TAG();
    ruby_cref = saved_cref;
    POP_SCOPE();
    POP_FRAME();
    POP_CLASS();
    POP_VARS();
    ruby_wrapper = wrapper;
    if (ruby_nerrs > 0) {   /* パースエラーが起きた */
        ruby_nerrs = 0;
        rb_exc_raise(ruby_errinfo);
    }
    if (state) jump_tag_but_local_jump(state);
    if (!NIL_P(ruby_errinfo))   /* ロード中に例外が発生した */
        rb_exc_raise(ruby_errinfo);
}

やっとスタック操作の嵐から抜けられたと思った瞬間また突入するというのも 精神的に苦しいものがあるが、気を取りなおして読んでいこう。

長い関数の常で、コードのほとんどがイディオムで占められている。 PUSH/POP、タグプロテクトと再ジャンプ。その中でも注目したいのは (A)のCREF関係だ。ロードしたプログラムは常にトップレベル上で 実行されるので、ruby_crefを(プッシュではなく)退避しtop_crefに戻す。 ruby_frame->cbaseも新しいものにしている。

それともう一ヶ所、(B)でなぜかruby_in_evalをオンにしている。そもそも この変数はいったい何に影響するのか調べてみると、rb_compile_error()とい う関数だけのようだ。ruby_in_evalが真のときは例外オブジェクトにメッセージを 保存、そうでないときはstderrにメッセージを出力、となっている。つまりコ マンドのメインプログラムのパースエラーのときはいきなりstderrに出力した いのだが評価器の中ではそれはまずいので止める、という仕組みらしい。すると ruby_in_evalのevalはメソッドevalや関数eval()ではなくて一般動詞の evaluateか、はたまたeval.cのことを指すのかもしれない。

rb_load_file()

ここでソースファイルは突然ruby.cへと移る。と言うよりも実際のところは こうではないだろうか。即ち、ロード関係のファイルは本来ruby.cに置きたい。 しかしrb_load()ではPUSH_TAG()などを使わざるを得ない。だから仕方なく eval.cに置く。でなければ最初から全部eval.cに置くだろう。

それで、rb_load_file()だ。

rb_load_file()

 865  void
 866  rb_load_file(fname)
 867      char *fname;
 868  {
 869      load_file(fname, 0);
 870  }

(ruby.c)

まるごと委譲。load_file()の第二引数scriptは真偽値で、rubyコマンドの 引数のファイルをロードしているのかどうかを示す。今はそうではなく ライブラリのロードと考えたいのでscript=0で疊み込もう。 さらに以下では意味も考え本質的でないものを削ってある。

load_file()(簡約版)

static void
load_file(fname, /* script=0 */)
    char *fname;
{
    VALUE f;
    {
        FILE *fp = fopen(fname, "r");   (A)
        if (fp == NULL) {
            rb_load_fail(fname);
        }
        fclose(fp);
    }
    f = rb_file_open(fname, "r");       (B)
    rb_compile_file(fname, f, 1);       (C)
    rb_io_close(f);
}

(A)実際にfopen()で開いてみて本当に開けるかどうかチェックをする。 大丈夫ならすぐに閉じる。無駄ではあるが、非常にシンプルかつ移植性が 高くしかも確実な方法だ。

(B)改めてRubyレベルのライブラリFile.openで開く。 最初からFile.openで開かないのはRubyの例外が発生してしまわないように するためである。今は何か例外が起きたらロードエラーにしたいので、 open関係のエラー、例えばErrno::ENOENTとかErrno::EACCESSとか ……になっ てしまうと困るのだ。ここはruby.cなのでタグジャンプを止めることはできない。

(C)パーサインターフェイスrb_compile_file()IOオブジェクトから プログラムを読み、構文木にコンパイルする。 構文木はruby_eval_treeに追加されるので結果を受け取る必要はない。

ロードのコードは以上だ。最後に、呼び出しがかなり深かったので rb_f_require()以下のコールグラフを載せておく。

rb_f_require           ....eval.c
    rb_find_file            ....file.c
        dln_find_file           ....dln.c
            dln_find_file_1
    rb_load
        rb_load_file            ....ruby.c
            load_file
                rb_compile_file     ....parse.y
        eval_node

長旅のお供にコールグラフ。もはやこれは常識だ。

ロードに必要なopenの数

先程ファイルが開けるかどうかチェックするためだけに使われるopenがあった が、実はrubyのロードの過程ではその他にrb_find_file_ext()などでも内部で openしてチェックしている。全体ではいったい何回くらいopen()しているのだ ろう。

と思ったら実際に数えてみるのが正しいプログラマのありかただ。システムコー ルトレーサを使えば簡単に数えられる。そのためのツールはLinuxなら strace、Solarisならtruss、BSD系ならktracetruss、 というように OSによって名前がてんでバラバラなのだが、Googleで検索すればすぐ見付かる はずだ。WindowsならたいていIDEにトレーサが付いている。

さて、筆者のメイン環境はLinuxなのでstraceで見てみた。 出力がstderrに出るので2>&1でリダイレクトしている。

% strace ruby -e 'require "rational"' 2>&1 | grep '^open'
open("/etc/ld.so.preload", O_RDONLY)    = -1 ENOENT
open("/etc/ld.so.cache", O_RDONLY)      = 3
open("/usr/lib/libruby-1.7.so.1.7", O_RDONLY) = 3
open("/lib/libdl.so.2", O_RDONLY)       = 3
open("/lib/libcrypt.so.1", O_RDONLY)    = 3
open("/lib/libc.so.6", O_RDONLY)        = 3
open("/usr/lib/ruby/1.7/rational.rb", O_RDONLY|O_LARGEFILE) = 3
open("/usr/lib/ruby/1.7/rational.rb", O_RDONLY|O_LARGEFILE) = 3
open("/usr/lib/ruby/1.7/rational.rb", O_RDONLY|O_LARGEFILE) = 3
open("/usr/lib/ruby/1.7/rational.rb", O_RDONLY|O_LARGEFILE) = 3

libc.so.6openまではダイナミックリンクの実装で使っているopenなので 残りのopenは計四回。つまり三回は無駄になっているようだ。

拡張ライブラリのロード

rb_f_require()load_dyna

さて今度は拡張ライブラリのロードである。まずはrb_f_require()load_dynaのところから行く。ただしロックまわりのコードはもういら ないので削った。

rb_f_require()load_dyna

5607  {
5608      int volatile old_vmode = scope_vmode;
5609
5610      PUSH_TAG(PROT_NONE);
5611      if ((state = EXEC_TAG()) == 0) {
5612          void *handle;
5613
5614          SCOPE_SET(SCOPE_PUBLIC);
5615          handle = dln_load(RSTRING(fname)->ptr);
5616          rb_ary_push(ruby_dln_librefs, LONG2NUM((long)handle));
5617      }
5618      POP_TAG();
5619      SCOPE_SET(old_vmode);
5620  }
5621  if (state) JUMP_TAG(state);

(eval.c)

もはやほとんど目新しいものはない。タグはイディオム通りの使いかた しかしていないし、可視性スコープの退避・復帰も見慣れた手法だ。 残るのはdln_load()だけである。これはいったい何をしているのだろう。 というところで次に続く。

リンクについて復習

dln_load()は拡張ライブラリをロードしているわけだが、拡張ライブラリを ロードするとはどういうことなのだろうか。それを話すにはまず話を思い切り 物理世界方向に巻き戻し、リンクのことから始めなければならない。

Cのプログラムをコンパイルしたことはもちろんあると思う。筆者は Linuxでgccを使っているので、次のようにすれば動くプログラムが 作成できる。

% gcc hello.c

ファイル名からするときっとこれはHello, World!プログラムなんだろう。 gccはUNIXではデフォルトでa.outというファイルにプログラムを 出力するので続いて次のように実行できる。

% ./a.out
Hello, World!

ちゃんとできている。

ところで、いまgccは実際には何をしたのだろうか。普段はコンパイル、 コンパイルと言うことが多いが、実際には

  1. プリプロセス(cpp
  2. C言語をアセンブラにコンパイル(cc
  3. アセンブラを機械語にアセンブル(as
  4. リンク(ld

という四つの段階を通っている。このうちプリプロセス・コンパイル・アセン ブルまではいろいろなところで説明を見掛けるのだが、なぜかリンクの段階だ けは明文化されずに終わることが多いようだ。学校の歴史の授業では絶対に 「現代」まで行き着かない、というのと同じようなものだろうか。そこで本書 ではその断絶を埋めるべく、まずリンクとは何なのか簡単にまとめておくこと にする。

アセンブルまでの段階が完了したプログラムはなんらかの形式の 「オブジェクトファイル」 になっている。そのような形式でメジャーなものには以下のよう なものがある。

念のため言っておくが、オブジェクトファイル形式のa.outccの デフォルト出力ファイル名のa.outは全然別物である。例えば今時のLinuxで 普通に作ればELF形式のファイルa.outができる。

それで、このオブジェクトファイル形式がどう違うのか、という話はこのさい どうでもいい。今認識しなければならないのは、これらのオブジェクトファイ ルはどれも「名前の集合」と考えられるということだ。例えばこのファイルに 存在する関数名や変数名など。

またオブジェクトファイルに含まれる名前の集合には二種類がある。即ち

である。そしてリンクとは、複数のオブジェクトファイルを集めてきたときに 全てのオブジェクトファイルの「必要な名前の集合」が「提供する名前の集合」 の中に含まれることを確認し、かつ互いに結び付けることだ。つまり全ての 「必要な名前」から線をひっぱって、どこかのオブジェクトファイルが「提供 する名前」につなげられるようにしなければいけない(図2)。 このことを用語を使って 言えば、未定義シンボルを解決する(resolving undefined symbol)、となる。

(link)
図2: オブジェクトファイルとリンク

論理的にはそういうことだが、現実にはそれだけではプログラムは走らないわ けだ。少なくともCのプログラムは走らない。名前をアドレス(数)に変換し てもらわなければ動けないからだ。

そこで論理的な結合の次には物理的な結合が必要になる。オブジェクトファイ ルを現実のメモリ空間にマップし、全ての名前を数で置き換えないといけない。 具体的に言えば関数呼び出し時のジャンプ先アドレスを調節したりする。

そしてこの二つの結合をいつやるかによってリンクは二種類に分かれる。即ち スタティックリンクとダイナミックリンクである。スタティックリンクはコン パイル時に全段階を終了してしまう。一方ダイナミックリンクは結合のうちい くらかをプログラムの実行時まで遅らせる。そしてプログラムの実行時になっ て初めてリンクが完了する。

もっともここで説明したのは非常に単純な理想的モデルであって現実をかなり 歪曲している面がある。論理結合と物理結合はそんなにキッパリ分かれるもの ではないし、「オブジェクトファイルは名前の集合」というのもナイーブに過 ぎる。しかしなにしろこのあたりはプラットフォームによってあまりに動作が 違いすぎるので、真面目に話していたら本がもう一冊書けてしまう。 現実レベルの知識を得るためにはさらに 『エキスパートCプログラミング』\footnote{『エキスパートCプログラミング』Peter van der Linden著、梅原系訳、アスキー出版局、1996} 『Linkers&Loaders』@footnote{『Linkers&Loaders』John R.Levine著、榊原一矢監訳 ポジティブエッジ訳、オーム社、2001} あたりも読んでおくとよい。

真にダイナミックなリンク

さてそろそろ本題に入ろう。ダイナミックリンクの「ダイナミック」は当然 「実行時にやる」という意味だが、普通に言うところのダイナミックリンクだ と実はコンパイル時にかなりの部分が決まっている。例えば必要な関数の名前 は決まっているだろうし、それがどこのライブラリにあるかということももう わかっている。例えばcos()ならlibmにあるからgcc -lmという 感じでリ ンクするわけだ。コンパイル時にそれを指定しなかったらリンクエラーになる。

しかし拡張ライブラリの場合は違う。必要な関数の名前も、リンクするライブ ラリの名前すらもコンパイル時には決まっていない。文字列をプログラムの実 行中に組み立ててロード・リンクしなければいけないのである。つまり先程の 言葉で言う「論理結合」すらも全て実行時に行わなければならない。そのため には普通に言うところのダイナミックリンクとはまた少し違う仕組みが必要に なる。

この操作、つまり実行時に全てを決めるリンク、のことを普通は 「動的ロード(dynamic load)」と呼ぶ。本書の用語遣いからいくと 「ダイナミックロード」と片仮名にひらくべきなのだろうが、 ダイナミックリンクと ダイナミックロードだと紛らわしいのであえて漢字で「動的ロード」とする。

動的ロードAPI

概念の説明は以上だ。あとはその動的ロードをどうやればいいかである。とは 言っても難しいことはなくて、普通はシステムに専用APIが用意されているの でこちらは単にそれを呼べばいい。

例えばUNIXならわりと広範囲にあるのがdlopenというAPIである。ただし 「UNIXならある」とまでは言えない。例えばちょっと前のHP-UXには全く違う インターフェイスがあるしMac OS XだとNeXT風のAPIを使う。また同じ dlopenでもBSD系だとlibcにあるのにLinuxだとlibdlとして外付けになっ ている、などなど、壮絶なまでに移植性がない。いちおうUNIX系と並び称され ていてもこれだけ違うわけだから、他のOSになれば全然違うのもあたりまえで ある。同じAPIが使われていることはまずありえない。

そこでrubyはどうしているかというと、その全然違うインターフェイスを吸収 するためにdln.cというファイルを用意している。dlnはdynamic linkの略だろ う。dln_load()はそのdln.cの関数の一つなのである。

そんなふうに全くバラバラの動的ロードAPIだが、せめてもの救 いはAPIの使用パターンが全く同じだということだ。どのプラットフォームだ ろうと

  1. ライブラリをプロセスのアドレス空間にマップする
  2. ライブラリに含まれる関数へのポインタを取る
  3. ライブラリをアンマップ

という三段階で構成されている。例えばdlopen系APIならば

  1. dlopen
  2. dlsym
  3. dlclose

が対応する。Win32 APIならば

  1. LoadLibrary(またはLoadLibraryEx
  2. GetProcAddress
  3. FreeLibrary

が対応する。

最後に、このAPI群を使ってdln_load()が何をするかを話そう。これが実は、 Init_xxxx()の呼び出しなのだ。ここに至ってついにruby起動から終了までの全 過程が欠落なく描けるようになる。即ち、rubyは起動すると評価器を初期化し なんらかの方法で受け取ったメインプログラムの評価を開始する。その途中で requireloadが起こるとライブラリをロードし制御を移す。制御を移す、と は、Rubyライブラリならばパースして評価することであり、拡張ライブラリな らばロード・リンクしてInit_xxxx()を呼ぶことである。

dln_load()

ようやくdln_load()の中身にたどりつけた。dln_load()も長い関数だが、これ また理由があって構造は単純である。まず概形を見てほしい。

dln_load()(概形)

void*
dln_load(file)
    const char *file;
{
#if defined _WIN32 && !defined __CYGWIN__
    Win32 APIでロード
#else
    プラットフォーム独立の初期化
#ifdef 各プラットフォーム
    ……プラットフォームごとのルーチン……
#endif
#endif
#if !defined(_AIX) && !defined(NeXT)
  failed:
    rb_loaderror("%s - %s", error, file);
#endif
    return 0;                   /* dummy return */
}

このようにメインとなる部分がプラットフォームごとに完璧に分離しているため、 考えるときは一つ一つのプラットフォームのことだけを考えていればいい。 サポートされているAPIは以下の通りだ。

dln_load()dlopen()

まずdlopen系のAPIのコードから行こう。

dln_load()dlopen()

1254  void*
1255  dln_load(file)
1256      const char *file;
1257  {
1259      const char *error = 0;
1260  #define DLN_ERROR() (error = dln_strerror(),\
                           strcpy(ALLOCA_N(char, strlen(error) + 1), error))
1298      char *buf;
1299      /* Init_xxxxという文字列をbufに書き込む(領域はalloca割り当て) */
1300      init_funcname(&buf, file);

1304      {
1305          void *handle;
1306          void (*init_fct)();
1307
1308  #ifndef RTLD_LAZY
1309  # define RTLD_LAZY 1
1310  #endif
1311  #ifndef RTLD_GLOBAL
1312  # define RTLD_GLOBAL 0
1313  #endif
1314
1315          /* (A)ライブラリをロード */
1316          if ((handle = (void*)dlopen(file, RTLD_LAZY | RTLD_GLOBAL))
                                                                 == NULL) {
1317              error = dln_strerror();
1318              goto failed;
1319          }
1320
              /* (B)Init_xxxx()へのポインタを取る */
1321          init_fct = (void(*)())dlsym(handle, buf);
1322          if (init_fct == NULL) {
1323              error = DLN_ERROR();
1324              dlclose(handle);
1325              goto failed;
1326          }
1327          /* (C)Init_xxxx()を呼ぶ */
1328          (*init_fct)();
1329
1330          return handle;
1331      }

1576    failed:
1577      rb_loaderror("%s - %s", error, file);
1580  }

(dln.c)

(A)dlopen()の引数のRTLD_LAZYは「実際に関数を要求したときに 未解決シンボルを解決する」ことを示す。返り値はライブラリを識別する ための印(ハンドル)で、dl*()には常にこれを渡さないといけない。

(B)dlsym()はハンドルの示すライブラリから関数ポインタを取る。返り値が NULLなら失敗だ。ここでInit_xxxx()へのポインタを取り、呼ぶ。

dlclose()の呼び出しはない。Init_xxxx()の中でロードした ライブラリの関数ポインタを 返したりしているはずだが、dlclose()するとライブラリ全体が使えなくなって しまうのでまずいのだ。つまりプロセスが終了するまでdlclose()は呼べない。

dln_load()−Win32

Win32ではLoadLibrary()GetProcAddress()を使う。 MSDNにも載っているごく一般的なWin32 APIである。

dln_load()−Win32

1254  void*
1255  dln_load(file)
1256      const char *file;
1257  {

1264      HINSTANCE handle;
1265      char winfile[MAXPATHLEN];
1266      void (*init_fct)();
1267      char *buf;
1268
1269      if (strlen(file) >= MAXPATHLEN) rb_loaderror("filename too long");
1270
1271      /* "Init_xxxx"という文字列をbufに書き込む(領域はalloca割り当て) */
1272      init_funcname(&buf, file);
1273
1274      strcpy(winfile, file);
1275
1276      /* ライブラリをロード */
1277      if ((handle = LoadLibrary(winfile)) == NULL) {
1278          error = dln_strerror();
1279          goto failed;
1280      }
1281
1282      if ((init_fct = (void(*)())GetProcAddress(handle, buf)) == NULL) {
1283          rb_loaderror("%s - %s\n%s", dln_strerror(), buf, file);
1284      }
1285
1286      /* Init_xxxx()を呼ぶ */
1287      (*init_fct)();
1288      return handle;

1576    failed:
1577      rb_loaderror("%s - %s", error, file);
1580  }

(dln.c)

LoadLibrary()してGetProcAddress()。ここまでパターンが同じだと 言うこともないので、終わってしまうことにしよう。


御意見・御感想・誤殖の指摘などは 青木峰郎 <aamine@loveruby.net> までお願いします。

『Rubyソースコード完全解説』 はインプレスダイレクトで御予約・御購入いただけます (書籍紹介ページへ飛びます)。

Copyright (c) 2002-2004 Minero Aoki, All rights reserved.