第2回 拡張ライブラリの作成(2)

前田修吾

究極の高速化

先日仕事でちょっとしたRubyスクリプトを書きました。これはXML文書を解析 するものなのですが、XMLParserという拡張ライブラリ[1]を利用するこ とで、簡単に書くことができました。XMLParserは、expat[2]というCで 記述された代表的なXMLパーザのラッパライブラリで、Rubyではおそらくもっ ともよく使われているXMLパーザです。ちなみに、最近では他にもpure rubyの (拡張ライブラリを利用しない)XMLパーザがいくつか実装されています。*1 その中のどれかをRubyの標準ラ イブラリにしてはどうか、という提案もあるのですが、まだどのライブラリが Ruby標準になるかは予断を許さない状況です。どのライブラリもインタフェイ スが異なるのでユーザにとっては悩ましいですね。

ところで、このスクリプトには同僚のS氏から後日注文がクレームが出ました。 実行速度が遅い、というのです。実際に運用する時のデータと同サイズのデー タを処理させると、十数秒ほどの実行時間になるそうで、「これでは実用にな らない」ということでした。こういうことを言われると燃えるタイプのプログ ラマも多いのでしょうが、筆者はどちらかというと「むむむ、面倒なことになっ たな」と思う方です。今回の場合は一応実行速度も考慮した上で拡張ライブラ リを利用することにした(他力本願です:p)ので、その上でなお遅いということ になると、速くするのはなかなか大変そうです。とりあえずプロファイラでボ トルネックを調べようかな、とか、Cで書かないといけないことになったら嫌 だなあ、などと考えていると、やっぱりこのままでよいという知らせが来まし た。PentiumIII 600MHzのマシンでは3秒ほどの実行時間だったそうで、実際に 運用するのPentiumIII800MHz程度の環境では問題ないだろうとのことでした。 ハードウェアの進歩というのはありがたいものですね。数行の拡張ライブラリ を書くだけでこんなに速くなりました、という結末を期待していた読者のみな さんには申し訳ないですが。

fnmatchライブラリ再び

さて、それではお約束通り、前回作成したfnmatch拡張ライブラリを改良して いくことにしましょう。

モジュールの利用

前回作成したfnmatch拡張ライブラリの問題点の一つは、printのようなグロー バルな関数風のメソッド(実際にはObjectのプライベートメソッド)として fnmatchメソッドを定義していたことです。これが問題なのは事実上グローバ ルな名前空間を使用するため、名前の衝突の危険性があるためです。(fnmatch という名前はあまり衝突しそうにありませんが、他の名前だった場合を想像し てみてください。)

この問題を回避する簡単な方法はモジュールを利用することです。本誌の後藤 謙太郎さんの連載を読まれていた方はすでにご存知だと思いますが、モジュー ルには二つの役割があります。一つはMix-inという限定された多重継承を実現 する役割で、もう一つは独立した名前空間を提供する役割です。ここで利用す るのは後者の名前空間を提供する役割で、代表的な例は標準ライブラリのMath モジュールです。Mathモジュールが提供するsqrtやsinといったメソッドはモ ジュール関数と呼ばれます。モジュール関数はモジュールをレシーバにして呼 び出したり、モジュールをインクルードした上でレシーバを省略して呼び出す こともできます。(List1)

-- List1 モジュール関数の利用例

  x = Math.sqrt(2)

  include Math
  x = sqrt(2)

今回はfnmatchメソッドをFnMatchというモジュール(他によい名前が思い付き ませんでした)のモジュール関数にすることにします。

引数の省略

二つ目の問題点はすべての引数を省略せずに指定しなければならなかったこと です。前回の仕様では特にフラグを指定したくない場合もfnmatch(pattern, str, 0)のように第3引数に0を指定する必要がありました。Rubyではこのよう な場合はList2のように引数の省略ができるようにするのが一般的です。

-- List2 引数の省略

  def fnmatch(pattern, str, flags = 0)
    ...
  end

拡張ライブラリの場合も、同様に引数の省略をサポートするAPIが用意されて います。今回は引数の省略もサポートすることにしましょう。

例外クラスの定義

三つ目の問題はfnmatch(3)がエラーを返した時にRuntimeErrorをraiseしてい たことです。RuntimeErrorは、

raise "error"

のように例外クラスを指定せずにraiseした場合に発生する例外です。つまり、 この例外をrescueする側にとってみれば、それがどのような例外かということ を表す情報がほとんどないことになります。これではあまりうれしくありませ んから、FnMatch::Errorという専用の例外クラスを定義することにしましょう。

ところで、Rubyスクリプトを書く時に、

begin
  ...
rescue
  ...
end

のように例外クラスを指定せずにrescueすることがあります。これは何か例外 が上がったらとにかくこの処理を行う、という場合に便利なのですが、プログ ラムのバグに起因するような例外までキャッチしてしまうので、デバッグの妨 げになる場合があります。したがって、ライブラリやある程度の規模のアプリ ケーションでは、なるべく、

begin
  ...
rescue IOError
  ...
end

のように例外クラスを特定するようにした方がよいです。(こういったことを 可能にするためにもFnMatch::Errorのような専用の例外クラスを定義する方が 望ましいわけです。)

ただ、Rubyでは、あるメソッドが発生する可能性のある例外を調べる方法が 言語的にサポートされていませんから、上記のようなコードを書くのはなかな か面倒です。ドキュメントに記述がない場合はソースコードのメソッド定義を 見るしかありません。しかも、そのメソッド定義でraiseされている例外を調 べるだけでは不十分で、そのメソッド内で呼んでいる他のメソッドで発生する 例外についても調べる必要があります。その点、Javaでは言語的なサポートが あるので楽ですね。(窮屈な面もありますが。)

改良版fnmatch拡張ライブラリ

それでは改良版のfnmatchライブラリのソースコード(List4)を見てみることに しましょう。extconf.rbについては前回と同じですが、一応List3に示してお きます。

-- List3 extconf.rb

  #!/usr/bin/ruby

  require "mkmf"

  if have_header("fnmatch.h")
    create_makefile("fnmatch")
  end

-- List4 fnmatch.c

  #include <fnmatch.h>
  #include "ruby.h"

  static mFnMatch;
  static eFnMatchError;

  static VALUE fnmatch_fnmatch(int argc, VALUE *argv, VALUE self)
  {
      VALUE pattern, string, flags;
      int ret, fl;

      if (rb_scan_args(argc, argv, "21", &pattern, &string, &flags) == 3) {
          fl = NUM2INT(flags);
      }
      else {
          fl = 0;
      }
      ret = fnmatch(STR2CSTR(pattern), STR2CSTR(string), fl);
      switch (ret) {
      case 0:
          return Qtrue;
      case FNM_NOMATCH:
          return Qfalse;
      default:
          rb_raise(eFnMatchError, "failed to fnmatch(3)");
      }
  }

  void Init_fnmatch()
  {
      mFnMatch = rb_define_module("FnMatch");
      rb_define_module_function(mFnMatch, "fnmatch", fnmatch_fnmatch, -1);
      rb_define_const(mFnMatch, "NOESCAPE", INT2NUM(FNM_NOESCAPE));
      rb_define_const(mFnMatch, "PATHNAME", INT2NUM(FNM_PATHNAME));
      rb_define_const(mFnMatch, "PERIOD", INT2NUM(FNM_PERIOD));

      eFnMatchError = rb_define_class_under(mFnMatch, "Error", rb_eStandardError);
  }
fnmatch_fnmatch()

fnmatch_fnmatch()はFnMatch::fnmatchメソッドの本体の処理を記述して います。前回はf_fnmatch()という名前でしたが、今回はモジュール関数 にしたので、「モジュー名_関数名」という名前に変更しています。

また、メソッドの引数の省略をサポートするためにfnmatch_fnmatch()関 数の引数はf_fnmatch()とは異なるものになっています。argcで与えられ た引数の数を、argvで引数の配列を、selfでレシーバを受け取ります。与 えられた引数の数のチェックなどは自分で行ってもよいのですが、 rb_scan_args()という便利な関数が用意されているので、ここではそれを 使っています。

rb_scan_args()は第3引数で与えられたフォーマットに従いargc・argvを チェックします。このフォーマットは1つ目の数字が最低限必要な引数の 数、2つ目の数字が付加的な引数の数となっており、2つ目の数字は省略可 能です。また最後に"*"が指定された場合は、さらに任意の数の引数を受 け付けることを示しています。この場合は"21"ですから、最低限必要な引 数が2つで、付加的な引数の数は1つです。第4引数以下はVALUE型の変数へ のポインタを指定し、解析された引数はこの変数に格納されます。ここで はpatternに最初の引数、stringに2番目の引数…といった具合になります。

rb_scan_args()は実際に与えられた引数の数を返します。vflagsが与えら れた場合はそれをintに変数したものをflagsに代入し、与えられなかった 場合はflagsに0を代入しています。

後の処理はほぼ前回のf_fnmatch()と同じですが、rb_eRuntimeErrorの代 りにeFnMatchErrorを利用しています。

Init_fnmatch()

まず、rb_define_module()でFnMatchというモジュールを作成し、 mFnMatchという変数に代入しています。(mFnMatchのmはmoduleのmです。) rb_define_module()はモジュールを作成する関数で、引数にはモジュール 名を指定します。改良版fnmatchライブラリではメソッドや定数や例外ク ラスなどは、すべてFnMatchモジュールの下に定義しています。

rb_define_module_function()はモジュール関数を定義します。引数はほ とんど前回利用したrb_define_global_function()と同じですが、第1引数 にはモジュールを指定し、それ以降の引数が1つずつ後にずれています。 引数の数は-1を指定していますが、これは可変長引数を配列で受け取ると いう意味です。rb_define_const()で定数を3つ定義しています。前回は FNM_NOESCAPEという名前で定数を定義していましたが、 FnMatch::FNM_NOESCAPEでは冗長なので、FnMatch::NOESCAPEという名前に 変更しました。最後にrb_define_class_under()でFnMatch::Errorという 例外クラスを定義しています。第1引数はクラスを定義するモジュール、 第2引数はクラス名、第3引数はスーパークラス(この場合は StandardError)です。通常クラス定義はrb_define_class()(_underが付か ない)を利用しますが、ここではFnMatchモジュールの下にクラスを定義す るために、rb_define_class_under()を利用しています。

基本設計の変更

さて、ここまでfnmatch拡張ライブラリの改良をしてきたわけですが、多少の 変更はあるものの、基本的にはfnmatchという関数的メソッドを提供するとい うインタフェイスはそのままでした。せっかくオブジェクト指向言語を使うの ですから、思い切って基本設計を変更し、WildCardというクラスを定義するこ とにしましょう。(Fig1)名前もwildcard拡張ライブラリとします。wildcardラ イブラリでは、マッチングを行うために、

wc = WildCard.new("*.h")
p wc.match("stdio.h")

のように[WildCardオブジェクトの生成]→[マッチング処理]という2つの段階 を踏むことになります。

Fig1 WildCardクラス

クラスメソッド

new(pattern, flags = 0)

WildCardオブジェクトを生成します。

メソッド

pattern
to_s

パターン文字列を返します。

flags

フラグを返します。

match(str)
self === str

strに対してマッチングを行います。

基礎知識

前回も簡単に触れましたが、拡張ライブラリで定義されたクラスのオブジェク トも含めて、CでRubyのオブジェクトを扱うためにはVALUEというデータ型を利 用します。この節では一歩進んで、VALUEとはどんなデータ型なのかとったこ とをお話します。ただ、VALUEが実際にどのように機能するのかを知らなくて も拡張ライブラリでクラス定義を行うことはできますから、わからない部分は 読みとばしていただいてもかまいません。

VALUEの実体

VALUE型はruby.hで定義されています。(List5) この定義を見ると、VALUEの実 体はunsinged longであるということがわかります。でもただのunsigned long でどうしてRubyのオブジェクトを扱うことができるのでしょう。勘のいい方な らすでにお気付きでしょうが、これにはタネがあります。

-- List5 ruby.h VALUE型の定義

  typedef unsigned long VALUE;

List6はArray#firstメソッドの定義です。ここでRARRAY()というマクロを利用 していますが、このマクロの定義がList7です。この定義にしたがって RARRAY()マクロを展開したものがList8です。これを見るとVALUEをRArray構造 体へのポインタ型にキャストしていることがわかります。実はVALUEの実体は ポインタだったのです。Rubyオブジェクトの実体はstructRArrayやstruct RStringといった構造体によって表現されます。そして、VALUEはそれらの構造 体へのポインタを整数型にキャストしたものなのです。VALUEからオブジェク トの情報を取り出したい時には、反対に構造体へのポインタにキャストしてや ります。

-- List6 array.c Array#firstの定義

  static VALUE
  rb_ary_first(ary)
      VALUE ary;
  {
      if (RARRAY(ary)->len == 0) return Qnil;
      return RARRAY(ary)->ptr[0];
  }

-- List7 ruby.h RARRAY()マクロ

  struct RArray {
      struct RBasic basic;
      long len, capa;
      VALUE *ptr;
  };
  ...
  #define R_CAST(st)   (struct st*)
  ...
  #define RARRAY(obj)  (R_CAST(RArray)(obj))

-- List8 マクロを展開したrb_ary_first()

  static VALUE
  rb_ary_first(ary)
      VALUE ary;
  {
      if (((struct RArray *) ary)->len == 0) return Qnil;
      return ((struct RArray *) ary)->ptr[0];
  }

ここで次のような疑問を持たれる方がいらっしゃるかもしれません。「longと ポインタのサイズが違う処理系では動作しないのではないか。」残念ながらそ の通りです。Rubyはlongとポインタのサイズが同じであることを仮定していま す。longとポインタのサイズが異なる場合はrubyのコンパイル時にエラーにな ります。(List9)

-- List9 ruby.h longとポインタのサイズチェック

  #if SIZEOF_LONG != SIZEOF_VOIDP
  ---->> ruby requires sizeof(void*) == sizeof(long) to be compiled. <<----
  #endif

しかし、なぜvoid *ではなくunsinged longなのでしょうか。それはVALUEで扱 う値は必ずしもポインタに限られるわけではなく、即値である場合もあるから です。たとえば、Fixnumのようなオブジェクトはよく使われるので、いちいち オブジェクトをヒープに割り当てるとかなりのコストがかかります。そこで、 Rubyでは、ヒープに割り当てられたオブジェクトを指すポインタの値が奇数で ない(最下位ビットが立っていない)ことを仮定して、奇数の領域にFixnumオブ ジェクトを即値として配置しています。具体的にはFixnumオブジェクトを VALUEで表現すると、その値を左に1ビットシフトして最下位ビットを立てた値 になります。(List10) たとえば、1124という値をlongとVALUE(Fixnum)で表現 した場合、Fig2のようになります。Fixnumの他にも、nil・true・falseや Symbolは、ポインタと重ならないような領域に即値として配置されています。

-- List10 longからFixnumへの変換

  #define FIXNUM_FLAG 0x01
  #define INT2FIX(i) ((VALUE)(((long)(i))<<1 | FIXNUM_FLAG))

-- Fig2 1124のlong表現とVALUE(Fixnum)表現

  long:   0|0 ... 0 1 0 0 0 1 1 0 0 1 0 0|
         +-+                           +-+
  VALUE: |0 ... 0 1 0 0 0 1 1 0 0 1 0 0|1

RData構造体

ヒープに割り当てられるオブジェクトは、StringならRString構造体、Arrayな らRArray構造体、といったように、それぞれ対応する構造体を持っています。 *2 これらの構造体は、オブジェクトのクラスやtaintされているかどうかなどを 示すフラグなどのすべてのクラス共通の情報と、文字列の内容や配列の長さな どの各クラス固有の情報を持ちます。

では拡張ライブラリでクラスを定義する時も、同じような構造体を用意する必 要があるのでしょうか。実はRData構造体というポインタをラップするための 汎用の構造体が標準で提供されており、これを利用すれば任意のポインタを Rubyオブジェクトでラップすることができます。ポインタをオブジェクトでラッ プしたり、ラップしたオブジェクトからポインタを取り出すためには、以下の ようなマクロを利用します。

Data_Wrap_Struct(VALUE klass, void (*mark)(), void (*free)(), void *sval)

ポインタをラップしたオブジェクトを生成して返します。klassは生成さ れるオブジェクトのクラス、markはGCのマークフェイズで呼ばれる関数へ のポインタ、freeはGCで回収された時に呼ばれる関数へのポインタ、sval はラップされるポインタです。

Data_Make_Struct(klass, type, mark, free, sval)

type型のデータをmalloc()し、そのデータへのポインタをsvalに代入して、 svalをラップしたオブジェクトを生成して返します。type以外の引数は Data_Wrap_Structと同じです。

Data_Get_Struct(obj, type, sval)

objからtype型のデータへのポインタを取り出し、svalに代入します。

wildcard拡張ライブラリ

では、wildcard拡張ライブラリのソースコードを見ることにしましょう。 (List12) ライブラリ名が変わっただけですが、一応extconf.rbもList11に示 しておきます。

-- List11 extconf.rb

  #!/usr/bin/ruby

  require "mkmf"

  if have_header("fnmatch.h")
    create_makefile("wildcard")
  end

-- List12 wildcard.c

  #include <fnmatch.h>
  #include "ruby.h"

  typedef struct wildcard {
      char *pattern;
      int flags;
  } wildcard_t;

  static VALUE cWildCard;
  static VALUE eWildCardError;

  static void wildcard_free(wildcard_t *wc)
  {
      free(wc->pattern);
      free(wc);
  }

  static VALUE wildcard_s_new(int argc, VALUE *argv, VALUE self)
  {
      VALUE obj, pattern, flags;
      int fl;
      wildcard_t *wc;

      if (rb_scan_args(argc, argv, "11", &pattern, &flags) == 2) {
          fl = NUM2INT(flags);
      }
      else {
          fl = 0;
      }
      Check_Type(pattern, T_STRING);
      obj = Data_Make_Struct(self, wildcard_t, NULL, wildcard_free, wc);
      wc->pattern = ALLOC_N(char, RSTRING(pattern)->len + 1);
      strncpy(wc->pattern, RSTRING(pattern)->ptr, RSTRING(pattern)->len);
      wc->pattern[RSTRING(pattern)->len] = '\0';
      wc->flags = fl;
      return obj;
  }

  static VALUE wildcard_pattern(VALUE self)
  {
      wildcard_t *wc;

      Data_Get_Struct(self, wildcard_t, wc);
      return rb_str_new2(wc->pattern);
  }

  static VALUE wildcard_flags(VALUE self)
  {
      wildcard_t *wc;

      Data_Get_Struct(self, wildcard_t, wc);
      return INT2NUM(wc->flags);
  }

  static VALUE wildcard_match(VALUE self, VALUE str)
  {
      wildcard_t *wc;
      int ret;

      Data_Get_Struct(self, wildcard_t, wc);
      ret = fnmatch(wc->pattern, STR2CSTR(str), wc->flags);
      switch (ret) {
      case 0:
          return Qtrue;
      case FNM_NOMATCH:
          return Qfalse;
      default:
          rb_raise(eWildCardError, "error");
      }
  }

  void Init_wildcard()
  {
      cWildCard = rb_define_class("WildCard", rb_cObject);
      rb_define_singleton_method(cWildCard, "new", wildcard_s_new, -1);
      rb_define_method(cWildCard, "pattern", wildcard_pattern, 0);
      rb_define_method(cWildCard, "to_s", wildcard_pattern, 0);
      rb_define_method(cWildCard, "flags", wildcard_flags, 0);
      rb_define_method(cWildCard, "match", wildcard_match, 1);
      rb_define_method(cWildCard, "===", wildcard_match, 1);
      rb_define_const(cWildCard, "NOESCAPE", INT2NUM(FNM_NOESCAPE));
      rb_define_const(cWildCard, "PATHNAME", INT2NUM(FNM_PATHNAME));
      rb_define_const(cWildCard, "PERIOD", INT2NUM(FNM_PERIOD));

      eWildCardError = rb_define_class_under(cWildCard, "Error", rb_eStandardError);
  }
wildcard_s_new()

wildcard_s_new()はWildCardオブジェクトを生成します。引数の処理など はfnmatchライブラリのfnmatch_fnmatch()とほとんど同じです。WildCard オブジェクトはpattern、flagsという2つのメンバを持つwildcard構造体 へのポインタをラップします。Data_Make_Struct()の第1引数にはselfを 指定していますが、selfはWildCardクラスを指します。(WildCardクラスの サブクラスを作成した場合は、サブクラスになります。)第4引数には wildcard_free()という関数を指定していますが、wildcard_free()は patternと構造体そのものをfree()します。pattern用のメモリ領域を割り 当てるためにALLOC_N(tyep,n)というマクロを利用していますが、これは ruby.hで提供されるマクロで、必要ならGCを起動してから、 sizeof(type) * nバイトの領域をmalloc()します。

wildcard_pattern()
wildcard_flags()

selfからwildcard構造体へのポインタを取り出し、それぞれpatternと flagsの値を返します。wildcard_pattern()ではrb_str_new2()という関数 を使用していますが、これは引数で指定した内容のStringオブジェクトを 生成する関数です。

wildcard_match()

wildcard_match()はselfからwildcard構造体へのポインタを取り出し、 patternとflagsの値を使ってマッチング処理を行います。処理自体は fnmatch_fnmatch()とほとんど同じです。

Init_wildcard()

WildCardというクラスを作成し、メソッドや定数を定義しています。 rb_define_singleton_method()は特異メソッドを定義する関数です。 この場合はクラスの特異メソッド、すなわちクラスメソッドを定義してい ます。また、rb_define_method()はインスタンスメソッドを定義します。

WildCard#matchの別名としてWildCard#===を用意していますが、これは caseやEnumerable#grepなどでWildCardオブジェクトを利用できるように するためです。(List13)

-- List13 caseやgrepでの利用

  HEADER_PATTERN = WildCard.new("*.h")
  SOURCE_PATTERN = WildCard.new("*.c")
  ...
  case filename
  when HEADER_PATTERN
    ...
  when SOURCE_PATTERN
    ...
  end
  ...
  sources = filenames.grep(SOURCE_PATTERN)

まとめ

今回はモジュールで関数的なインタフェイスを提供する方法と、クラスを定 義する、よりオブジェクト指向的な方法を説明しました。必ずしも、後者の方 が優れているとは限りませんので、ケースバイケースでより良い方法を選択し てください。ただ、Cのライブラリで、ある構造体を操作する関数群が提供さ れているような場合には、その構造体をラップするクラスを定義した方が自然 だと思います。

さて、2回に渡って拡張ライブラリの作成について説明してきましたが、いか がでしたでしょうか。説明が不十分な部分も多いと思いますが、足りない情報 はRubyのアーカイブに含まれるREADME.EXT.jpなどで補ってください。また、 拡張ライブラリの作成に関する話題を扱うruby-extというメーリングリスト [3]もあります。

次回はCアプリケーションへのRubyインタプリタの組み込みについて説明しま す。

参考URL

[1]

「XMLParserモジュール」, 吉田正人氏 <URL:http://www.yoshidam.net/Ruby_ja.html#xmlparser>

[2]

"expat - XML Parser Toolkit", James Clark <URL:http://www.jclark.com/xml/expat.html>

[3]

ruby-ext ML <URL:mailto:ruby-list-ctl@ruby-lang.org> <URL:http://www.ruby-lang.org/ja/ml.html>


*1 NQXML、ChibiXML、xmlscan、PXMLなど。
*2 クラスと構造体は必ずしも1対1対応になっているわけではありません。