4. 動的ライブラリ

動的ライブラリは、プログラムの起動時以外のときにロードされるライブラリです。 動的ライブラリは、プラグインやモジュールを実装するときに特に便利です。 というのは、必要になるまで、プラグインのロードを待機させることができるからです。 例えば、Pluggable Authentication Modules (PAM) システムでは、管理者が認証の設定/再設定をおこなえるよう、動的ライブラリを使用しています。 また、効率を上げるために途中で休止せずにその時々でコードをマシンコードにコンパイルし、そのコンパイルされたものを使用するというインタプリタを実装するのにも役に立ちます。 この方法は、例えばジャストインタイム・コンパイラや、マルチユーザ・ダンジョン (MUD) を実装するときに使えます。

Linux では、実際のところ、フォーマットという視点から見ると、動的ライブラリは特別なものではありません。 今まで述べてきたものと同じで、標準的なオブジェクトファイルや共有ライブラリとしてビルドされています。 主な違いは、動的ライブラリは、プログラムのリンク時や起動時に自動的にはロードされない、という点です。 そのかわりに、ライブラリをオープンし、シンボルを検索し、エラーを処理し、ライブラリを閉じる、という API が存在します。 この API を使用するためには、C プログラマはヘッダファイル <dlfcn.h> をインクルードする必要があります。

Linux で使用されるインターフェースは、基本的に Solaris のものと同じです。 これを「dlopen()」API と呼ぼうと思います。 しかし、このインターフェースは全てのプラットフォームでサポートされているわけではありません。 HP-UX では shl_load() という別の仕組みが使用され、Windows プラットフォームでは全く異なるインターフェースである DLL が使用されます。 広範な移植性を最終目標とするならば、おそらく、プラットフォーム間の差違を隠蔽するラッパーライブラリの使用を検討したほうがよいでしょう。 方法の一つとして、モジュールの動的ローディングをサポートする glib ライブラリがあります。 glib ライブラリは、プラットフォーム固有の動的ローディング・ルーチン群を内部で使用し、動的ローディング用の移植性の高いインターフェースを実装しています。 glib については、http://developer.gnome.org/doc/API/glib/glib-dynamic-loading-of-modules.html を参照してください。 glib のインターフェースについては、glib のドキュメントで十分に説明されているので、ここではこれ以上言及しません。 もう一つの方法は、libltdl を使う方法です。 libltdl は、GNU libtool の一部です。 もっと多くの機能が必要ならば、CORBA Object Request Broker (ORB) を調べてみるのもよいでしょう。 Linux と Solaris でサポートされるインターフェースを直接使用することにまだ興味をお持ちならば、読み進んでください。

C++ と動的ライブラリを使用する開発者の方は、「C++ dlopen mini HOWTO」も参照してください。

4.1. dlopen()

dlopen(3) 関数は、ライブラリをオープンし、使用前の準備をおこないます。 C のプロトタイプは次のようになります。

  void * dlopen(const char *filename, int flag);

ファイル名が「/」で始まるならば (つまり絶対パスならば)、dlopen() は単にそのファイル名を使おうとします (ライブラリを検索しようとはしません)。 それ以外ならば、dlopen() は次の順番でライブラリを検索します。

  1. ユーザの LD_LIBRARY_PATH 環境変数内のコロンで区切られたディレクトリリスト

  2. /etc/ld.so.cache 内に指定されたライブラリリスト (/etc/ld.so.cache は /etc/ld.so.conf をもとに生成されます)

  3. /lib, 次に /usr/lib. この順番に注意してください。この順番は、古い a.out ローダで使われていた順番とは逆です。 古い a.out ローダは、プログラムをロードするとき、最初に /usr/lib を検索し、そのあとで /lib を検索していました (man ページ ld.so(8) を参照してください)。 このことは、通常は問題にならないはずです。 なぜなら、ライブラリはどちらか一方のディレクトリのみに置いてあるはずだからです。 異なるライブラリに同じ名前を付けると、混乱を招きます。

dlopen() では、flag の値は RTLD_LAZY か RTLD_NOW のどちらかでなければなりません。 RTLD_LAZY は、「動的ライブラリのコードが実行されるときに未定義シンボルを解決せよ」という意味で、RTLD_NOW は、「dlopen() がリターンする前に全ての未定義シンボルを解決せよ、それができないようならば失敗せよ」という意味です。 RTLD_GLOBAL は、flag のどちらかの値と任意で論理和結合させることができます。 RTLD_GLOBAL フラグを付けてロードされたライブラリ内で定義されている外部シンボルは、後からロードされるライブラリからでも使用できるようになります。 デバッグ中は、おそらく RTLD_NOW を使いたくなるでしょう。 RTLD_LAZY を使うと、未解決の参照があったときに不可解なエラーが生成されます。 RTLD_NOW を使うと、ライブラリのオープンには若干時間がかかるようになります (しかし、のちのちの検索速度は速くなります)。 このことがユーザインターフェース上問題になるようでしたら、あとで RTLD_LAZY に切り替えることができます。

ライブラリが他のライブラリに依存しているなら (例えば、X が Y に依存している)、依存されているほうを先にロードしてください (この例で言えば、Y を先にロードし、それから X をロードします)。

dlopen() の戻り値は、他の動的ライブラリ・ルーチンによって使用される「ハンドル」です (このハンドルの実体は隠蔽されているものとして扱ってください)。 ロードが失敗した場合、dlopen() は NULL を返しますので、戻り値をチェックする必要があります。 同じライブラリが dlopen() で二回以上ロードされると、同じファイルハンドルが返されます。

古いシステムでは、ライブラリが _init という名前のルーチンをエクスポートしている場合、dlopen() から戻る前にそのコードが実行されます。 この仕組みを使って、ライブラリの初期化ルーチンを実装することができます。 しかし、ライブラリは、_init や _fini といった名前のルーチンをエクスポートすべきではありません。 これらの仕組みは古くなっており、望んでいない動作をする可能性があります。 かわりに、ライブラリでは、__attribute__((constructor)) 関数属性と __attribute__((destructor)) 関数属性を使用してルーチンをエクスポートしてください (gcc を使用しているものと仮定しています)。 詳細については Section 5.2 を参照してください。

4.2. dlerror()

dlerror() によりエラーを報告できます。 dlerror() は、最後の dlopen(), dlsym(), または dlclose() 呼出しのエラーについて説明する文字列を返します。 一つ変わっているのは、dlerror() を呼び出すと、以降の dlerror() の呼出しは他のエラーが発生するまで NULL を返すという点です。

4.3. dlsym()

動的ライブラリをロードしても、それを使えなければ意味がありません。 動的ライブラリを使用する上で中心となるルーチンは dlsym(3) です。 dlsym() は、与えられた (オープン済みの) ライブラリ内にあるシンボルの値を検索します。 この関数は次のように定義されています。

  void * dlsym(void *handle, char *symbol);

handle は dlopen() から返される値で、symbol はヌル文字で終端された文字列です。 dlsym() の結果を void * ポインタに格納することは、できるだけ避けてください。 なぜなら、そのポインタを使用するたびにキャストしなければならなくなるからです (そのプログラムをメンテナンスしようとする他の方々が受け取れる情報量も減ってしまいます)。

dlsym() は、シンボルが見つからなければ NULL を返します。 シンボルが NULL もしくはゼロという値をとることはありえないと分かっていれば、問題ありません。 しかし、そうでない場合は潜在的に曖昧さが残ります。もしも NULL を受け取った場合、それは、「そんなシンボルは存在しない」ということを意味するのでしょうか、それとも「そのシンボルの値は NULL である」ということを意味するのでしょうか? 標準的な方法は、(存在している可能性のあるエラー条件をクリアするために) まず最初に dlerror() を呼び、次に、シンボルを要求するために dlsym() を呼び、最後に、エラーが発生しているかどうかを調べるために再度 dlerror() を呼び出す、というものです。 コードは次のようになるでしょう。

  dlerror(); /* エラーコードをクリアする。*/
  s = (actual_type) dlsym(handle, symbol_being_searched_for);
  if ((err = dlerror()) != NULL) {
      /* ハンドルエラー。シンボルは見つからなかった。*/
  } else {
      /* シンボルが見つかった。値は s に格納されている。*/
  }

4.4. dlclose()

dlopen() の逆が dlclose() です。dlclose() で動的ライブラリをクローズします。 dl ライブラリは動的なファイルハンドルへのリンク数を管理しているので、同一の動的ライブラリに対して、dlopen() が成功した回数と同じ数の dlclose() が呼ばれない限り、当該ライブラリは実際にはメモリ上から削除されません。 ですので、同じプログラムが同じライブラリを何回ロードしても、問題にはなりません。 ライブラリの割当てが解除されるとき、古いライブラリでは、(もしも存在するならば) _fini 関数が呼ばれます。 しかし、_fini は古い仕組みなので、これに依存してはいけません。 かわりに、ライブラリでは、__attribute__((constructor)) 関数属性と __attribute__((destructor)) 関数属性を使用してルーチンをエクスポートしてください。 詳細については Section 5.2 を参照してください。 注意:dlclose() は、成功ならば 0 を、エラーならば非ゼロを返します。 この返り値について言及していない Linux man ページもあります。

4.5. 動的ライブラリの例

dlopen(3) の man ページ内の例をここに掲載します。 この例では、数学ライブラリをロードし、2.0 のコサインを出力しています。 また、全てのステップでエラーをチェックしています (推奨)。


  #include <stdlib.h>
  #include <stdio.h>
  #include <dlfcn.h>

  int main(int argc, char **argv) {
      void *handle;
      double (*cosine)(double);
      char *error;

      handle = dlopen ("/lib/libm.so.6", RTLD_LAZY);
      if (!handle) {
          fputs (dlerror(), stderr);
          exit(1);
      }

      cosine = dlsym(handle, "cos");
      if ((error = dlerror()) != NULL)  {
          fputs(error, stderr);
          exit(1);
      }

      printf ("%f\n", (*cosine)(2.0));
      dlclose(handle);
  }

このプログラムが "foo.c" という名前のファイルだとすると、次のコマンドでプログラムを作成することができます。

  gcc -o foo foo.c -ldl