前田修吾
Ruby hackerのみなさん、こんにちは。今回から数回の間、後藤謙太郎さんの 代役をつとめさせていただくことになりました。
あちこちで、後藤さんの記事をもっと読みたい、という声を耳にするので、書 きづらいことこの上ないのですが、何とかがんばって書いていきますので、温 かい目で見守ってやってください。(いや、温かい目で見守らなくてもいいで すから、C MAGAZINEを買うのをやめたりしないでください。)
さて、第1回は拡張ライブラリの作成方法についての話です。Rubyの連載のく せにRubyのコードがあまり出て来ませんが、どうかご容赦を。
拡張ライブラリというのはCで記述されたRuby用のライブラリのことです。と いうとStringやArrayなどの標準ライブラリも拡張ライブラリなのか、とおっ しゃる方もいるかもしれませんが(筆者もどちらかというとそういう天の邪鬼な 人間です)、「拡張」というとおり、Rubyの機能を拡張するためのライブラリ のことを指すので、標準ライブラリは拡張ライブラリとは呼びません。
拡張ライブラリは以前は拡張モジュールと呼ばれていたのですが、Rubyではモ ジュールという用語に別の意味があるので、混乱を避けるため、拡張ライブラ リと呼ばれるようになりました。
余談になりますが、コンピュータ用語には同じようなものを指す言葉が色々あっ たり、同じ言葉が違うものを指していたりして、混乱することが多いですね。 たとえば、Rubyではオブジェクトを(ファイルに保存したり、ネットワークで 転送するために)バイト列に変換することをmarshalと呼びますが、同じことを Javaではserializeと呼びます。また、serializeは文脈によっては排他制御の ような意味になりますので、どちらの意味で使っているのか確認しないと議論 がかみ合わなくなってしまうこともあります。[1] ニュースグループな どで議論する時は気を付けてくださいね。とにかく、Rubyでは「拡張ライブラ リ」ですので、お間違えのなきよう。
拡張ライブラリの例を挙げると、たとえば、標準添付されているsocketライブ ラリなども拡張ライブラリです。Rubyのソースパッケージを展開するとextと いうディレクトリが作成されますが、このディレクトリの下に置かれているラ イブラリ(socketもこれに含まれます)はみな拡張ライブラリなのです。また、 拡張ライブラリはRubyのソースアーカイブに含まれるもの以外にも、様々なも のがネットワーク上で配布されており、RAAなどで入手することができます。
すでに述べたように拡張ライブラリはCで記述されるのですが、ユーザから見 れば、拡張ライブラリもRubyで記述されたライブラリと同じように見えます。
require "socket"
とすれば、ライブラリをロードすることができますし、
s = TCPSocket.new("localhost", "smtp")
のように、拡張ライブラリで定義されたクラスやメソッドなどは、Rubyで定義 されたものと同じように扱うことができます。
つまり、ユーザから見れば拡張ライブラリであろうと、Rubyで書かれたライブ ラリであろうと関係ないわけです。拡張ライブラリについて何も知らなくても、 実はすでに使っていた、という方も多いかもしれませんね。
では、Rubyで書けば煩わしいメモリ管理などから一切解放されるのに、なぜわ ざわざ苦労してCでライブラリを書く必要があるのでしょうか。これには主に 二つの理由があります。
世の中にC用のライブラリは非常にたくさん出回っています。一方Ruby用のラ イブラリはそれほど多くありません。というより少ないと言った方がよいでしょ う。ですから、C用のライブラリをRubyで使いたい、という要求が起こってく るのは当然のことです。ただ、残念ながら、C用のライブラリをそのままRuby で利用することはできません。
そこで拡張ライブラリを利用することになります。Cライブラリを利用するた めの拡張ライブラリを用意することによって、関接的にRubyからCライブラリ を利用するわけです。拡張ライブラリを書く人はちょっと面倒ですが、利用す る側はCのことを意識する必要はないので非常に便利です。
Rubyのソースアーカイブに標準添付されている拡張ライブラリはすべてCライ ブラリを利用するためのものです。RAAに登録されているものも大部分がそう です。拡張ライブラリはRubyからCライブラリを利用するためにあるといって もよいでしょう。
このような拡張ライブラリはラッパーライブラリ(wrapper library)と呼ばれ ます。これはCライブラリを包む(wrapする)ライブラリという意味です。苦い 薬をオブラートで包んで飲むようなものですね。筆者はオブラートも苦手なので すが、拡張ライブラリはオブラートよりずっとスマートですので、ご心配なく。
Rubyはインタプリタなので、コンパイラにくらべるとプログラムの実行速度は 遅くなっています。また、Rubyという言語は非常に動的な言語なので最適化の 余地が少なく、速度的に不利な要因が多くなっています。もちろん、Rubyプロ グラムも工夫することによってある程度速度を上げることができますが、やは り限界があります。
そこで、どうしても速度的な問題があるという場合に、拡張ライブラリを作成 することによってプログラムの高速化を図ることができます。拡張ライブラリ はCで書きますから、当然拡張ライブラリの部分の実行速度はCと同じになりま す。場合によってはRubyで書いた場合とはくらべものにならないほどに高速化 することができます。
ただ、気を付けないといけないのは、何でもかんでも拡張ライブラリにしてし まうとRubyを使う意味がなくなってしまうということです。高速化するために 拡張ライブラリを使うのは、どうしても必要な場合だけに限定した方がよいで しょう。たとえば、一部の人々はRubyを数値計算に利用していますが、さすが にすべてRubyで書くと性能に問題があるため、大量の計算が必要な部分は拡張 ライブラリで実現しているそうです。逆に言えば数値計算のような分野でも、 Rubyのような柔軟性が速度よりも重要になる部分があるということですね。
ちなみに現在、Rubyはまつもとさんの手によってバイトコードインタプリタ化 が進められています*1ので、Rubyの性能は今後飛躍的に向上するか もしれません。この記事の執筆時点ではまだ非常に基本的な部分しか実装され ていないのですが、特定のベンチマークプログラムでは現在のインタプリタの 3倍くらいの性能になっているそうです。それでもまだ、Schemeの実装の一つ であるQScheme[2]にくらべると3倍くらい遅いようで、さらなる高速化 が進められています。数年後にはRubyは遅いなんて誰も言わなくなっているか もしれませんね。
ここで、簡単に拡張ライブラリの構成について説明しておきましょう。拡張ラ イブラリは以下のようなファイルによって構成されます。
extconf.rbは環境に合ったMakefileを生成する設定スクリプトです。通常、 拡張ライブラリを配布する場合は、かならずextconf.rbをアーカイブに含 めます。extconf.rbはへッダファイルやライブラリの存在をチェックし、 必要なものが揃っている場合にだけMakefileを生成します。
Cライブラリにアクセスするためのメソッドやクラスを定義するCのソース ファイルです。ソースファイルが一つしかない場合は「ライブラリ名.c」 という名前にします。ソースファイルが複数ある場合は「ライブラリ名.c」 というファイル名を付けるとまずいので注意してください。
Makefileに依存関係を追加するためのファイルです。dependが存在する場 合は、extconf.rbによってMakefileが生成される時に、末尾にdependの内 容が追加されます。このファイルは必須ではありません。
拡張ライブラリを構成するファイルのリストを記述します。拡張ライブラ リのディレクトリで、
$ find * -type f -print > MANIFEST
とすることで作成することができます。このファイルは拡張ライブラリを コンパイルする時にはとくに利用されませんが、パッケージング時などに 便利なので用意しておいた方がよいでしょう。
場合によってはへッダファイルを用意したり、データファイルなどを用意する こともあるかもしれませんが、だいたいこのような構成になっています。
では、実際に拡張ライブラリを作ってみることにしましょう。拡張ライブラリ の作成に必要な知識については、作例に沿う形で説明していきます。
作例に何を選ぶかで迷ったのですが、ちょうどruby-devでfnmatch(3)の機能を Rubyで提供してはどうか、という提案がされていました[3]ので、 fnmatch(3)の機能を提供する拡張ライブラリを作ることにします。ひょっとす るとみなさんがこの記事を読まれる頃には、fnmatch(3)の機能は標準で提供さ れているかもしれませんが、現時点ではまだ提供されていませんので、拡張ラ イブラリを利用するというのはリーズナブルな選択です。
fnmatch(3)はシェルのワイルドカードによるマッチングを行う関数で、多くの システムで標準で提供されています。
fnmatch(3)はfnmatch.hというへッダファイルに以下のようなプロトタイプ宣 言を持ちます。
int fnmatch(const char *pattern, const char *string, int flags);
第1引数patternはワイルドカードパターンで、第2引数のstringがマッチング の対象となる文字列です。第3引数のflagsはマッチング処理の挙動を指定する ためのフラグです。
マッチした場合は0を、マッチしなかった場合はFNM_NOMATCHを、エラーの場合 はそれ以外の値を返します。
詳しくは、
$ man 3 fnmatch
としてfnmatch(3)のマニュアルを参照してください。
実際に拡張ライブラリを作成する前に、まず、どのようなインタフェイスにす るのか設計しなければいけません。本来ならどのようなインタフェイスがよい のかということをしっかり検討すべきなのですが、これは最初の拡張ライブラ リなので、とりあえず安直にCと同じインタフェイスをそのまま採用すること にします。
具体的には以下のような関数風のメソッド(printやgetsなどと同じようにどこ からでもレシーバを省略した形で呼ベるメソッド)を定義することにします。
patternにstringがマッチする場合trueを返し、マッチしない場合falseを 返す。エラーの時は例外が発生する。
拡張ライブラリの名前も単純にfnmatchとします。
無事に設計も済んだ(?)ところで、extconf.rbを作成します。(List1) mkmfと いうライブラリをrequireしていますが、これはMakefileを作成するために必 要な機能を提供するライブラリです。mkmfでは以下のようなメソッドが提供さ れます。
ヘッダファイルの存在をチェックする。
ライブラリと関数の存在をチェックする。
関数の存在をチェックする。
Makefileを生成する。
このextconf.rbではhave_headerでfnmatch.hの存在をチェックし、 fnmatch.hが存在する場合はcreate_makefileでMakefileを作成しています。 create_makefileの引数には拡張ライブラリの名前を指定します。
-- List1 extconf.rb #!/usr/bin/ruby require "mkmf" if have_header("fnmatch.h") create_makefile("fnmatch") end
さて、いよいよソースファイル本体を作成します。List2を見ると、わずか25 行と拍子抜けするくらいに短いので、一部の抜粋かと思われる方もいらっしゃ るかもしれませんが、これで全部です。拡張ライブラリの作成と聞いて、何と くなく難しそうだと思われていた方も、安心されたのではないでしょうか。
-- List2 fnmatch.c #include <fnmatch.h> #include "ruby.h" static VALUE f_fnmatch(VALUE self, VALUE pattern, VALUE string, VALUE flags) { int ret; ret = fnmatch(STR2CSTR(pattern), STR2CSTR(string), NUM2INT(flags)); switch (ret) { case 0: return Qtrue; case FNM_NOMATCH: return Qfalse; default: rb_raise(rb_eRuntimeError, "error"); } } void Init_fnmatch() { rb_define_global_function("fnmatch", f_fnmatch, 3); rb_define_global_const("FNM_NOESCAPE", INT2NUM(FNM_NOESCAPE)); rb_define_global_const("FNM_PATHNAME", INT2NUM(FNM_PATHNAME)); rb_define_global_const("FNM_PERIOD", INT2NUM(FNM_PERIOD)); }
fnmatch.cでは次のような二つの関数を定義しています。
f_fnmatch()はfnmatchメソッドの本体の処理を記述しています。このよう に、関数的なメソッドの本体を定義する関数名は「f_メソッド名」とする という慣習になっています。f_というプリフィックスはfunctionを意味し ます。
戻り値と引数の型はすべてVALUEになっていますが、Rubyのオブジェクト はすべて、CではVALUEという型によって表現されます。拡張ライブラリで は必要に応じて、VALUE型のデータをintなどのCで扱いやすいデータに変 換して処理を行うことになります。
f_fnmatch()は引数を4つ持っていますが、最初の引数はメソッドのレシー バで、残りの3つがメソッドの引数です。この場合は関数的なメソッドな のでレシーバは利用しません。patternとstringはSTR2CSTR()というマク ロによってchar*型に、flagsはNUM2INT()というマクロによってint型 に変換し、それらを引数としてfnmatch(3)を呼び出しています。
マッチした場合はQtrueを、マッチしなかった場合はQfalseを返していま すが、これらはそれぞれRubyのtrue/falseを表します。f_fnmatch()の戻 り値はそのままfnmatchメソッドの戻り値になります。エラー時には rb_raise()によって例外を発生させています。rb_raise()の第一引数は例 外クラスを指定し、第2引数以降はprintf()と同じような引数を例外につ いての情報として渡します。ここでは第1引数にrb_eRuntimeErrorを与え ていますが、rb_eRuntimeErrorはRuntimeErrorクラスを表すVALUE型のグ ローバル変数で、ruby.hで宣言されています。Rubyではクラスもオブジェ クトなので、VALUE型で表現されます。
Init_fnmatch()は拡張ライブラリの初期化を行う関数です。拡張ライブラ リがロードされる時には「Init_ライブラリ名」という名前の関数が自動 的に呼ばれます。
Init_fnmatch()で行っているのは、fnmatchメソッドの定義と、3つのグロ ーバル定数の定義です。rb_define_global_function()はprintのようなグ ローバル関数風のメソッドの定義を行います。第1引数はメソッド名、第2 引数は実際の処理を行う関数へのポインタ、第3引数はメソッドの引数の 数を指定します。rb_define_global_const()はグローバル定数(Objectの 定数)を定義します。第1引数は定数名、第2引数は定数の値です。ここで はfnmatchメソッドの第3引数に利用するためのフラグを定義しています。 INT2NUM()というマクロを利用していますが、これはNUM2INT()と反対に int型のデータをVALUE型のRubyオブジェクトに変換します。
この拡張ライブラリは非常に規模の小さいものですが、大規模な拡張ライブラ リを作成する場合も基本的には同じような構成になります。拡張ライブラリの 初期化はかならず「Init_ライブラリ名」という名前の関数で行われるので、 「Init_ライブラリ名」を見ればその拡張ライブラリがどのようなインタフェ イスを提供しているのかを知ることができます。拡張ライブラリのソースを読 む場合は、まず「Init_ライブラリ名」関数から読み始めるとよいでしょう。
さて、ではこの拡張ライブラリをインストールしてみましょう。インストール の手順は以下のようになります。プロンプトが「#」になっている部分はroot 権限で実行してください。
$ ruby extconf.rb $ make # make site-install
site-installというターゲットを使うと、拡張ライブラリはsocketなどの標準 添付ライブラリとは別の各サイト用のディレクトリにインストールされます。 標準添付ライブラリと同じ場所にインストールしたい場合は代りにinstallと いうターゲットを利用してください。Debian GNU/Linuxのようなバイナリパッ ケージが整備された環境では、手動でインストールする拡張ライブラリは site-installを使うようにして、バイナリパッケージと区別した方が管理しや すいと思います。
List3はfnmatchライブラリを利用したサンプルプログラムです。標準入力や ファイルの各行を読み込み、コマンドライン引数で指定されたワイルドカード にマッチした行のみ出力するプログラムです。grepのワイルドカード版なので、 re(regular expression)をwc(wildcard)に変えて、gwcp.rbという名前にしま した。(サザエさんのしゃっくりのように発音してください。)
-- List3 gwcp.rb #!/usr/bin/ruby require "fnmatch" pattern = ARGV.shift unless pattern $stderr.printf("usage: %s <pattern> [files...]\n", $0) exit 1 end while line = gets line.chomp! if fnmatch(pattern, line, 0) puts line end end
ほとんど説明の必要がないくらい簡単なスクリプトですが、line.chomp!とし ている点に注意してください。chomp!は文字列の最後の改行コードを取り除く メソッドです。"*.txt"というパターンに"foo.txt\n"ではマッチしないので最 後の改行コードを取り除いているわけです。printの代りにputsを利用するこ とで出力には再び改行コードを追加しています。
Fig1はgwcp.rbの実行例で、RubyのMANIFESTファイルからへッダファイルを抜 き出しています。'*.h'のようにクウォートして、シェルのワイルドカード展 開を抑制している点に注意してください。
-- Fig1 gwcp.rbの実行例 $ ruby gwcp.rb '*.h' ~/src/ruby/MANIFEST defines.h dln.h env.h intern.h node.h re.h ...
さて、これでまずは動作する拡張ライブラリを作成することができました。思っ たより簡単だったのではないでしょうか。
しかし、今回作成したfnmatchライブラリの設計はお世辞にもよいと言えるも のではありません。(はっきり言ってしまうとダサいです。) これには理由が あります。それはいきなり完成品を作ってしまうとネタがなくなるから、では なくて、簡単なことからはじめて、段階的にステップアップしていった方がわ かりやすいと思うからです。
というわけで、次回は今回作成したライブラリを改良しながら、もう少し拡張 ライブラリの作成について説明します。
それでは、Happy hacking!
"Re: Concurrent Programming in Java", kuno@gssm.otsuka.tsukuba.ac.jp <URL:http://www.shugo.net/news/fj.comp.oops/19970219-27.txt>
"QScheme", Daniel Crettol <URL:http://www.sof.ch/dan/qscheme/index-e.html>
"[ruby-dev:12197] String#fnmatch", Akinori MUSHA <URL:http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-dev/12197>
*1 現在のインタプリタでは構文木を再帰的に辿って実行
する仕組みになっています。