# TransTECK 2000年3月号特集原稿 # 『Ruby/Kakasi拡張モジュール』 # # (c) GOTO Kentaro 2000 # # 後藤謙太郎(北大院数学) # # create: 2000/01/16 # status: final # # 概要 # 1. 拡張ライブラリとは # 2. kakasi とは # 3. なぜ拡張ライブラリなのか # 4. RubyのGC # 5. 仕様 # 6. 実装と注意点 # 7. フィードバック # 8. REFERENCES =begin = Ruby/KAKASI拡張モジュール <<< note == 拡張モジュールとは RubyやPerl,Pythonといったスクリプト言語では,機能を拡張するためにCな どのコンパイラ言語で書かれたライブラリのことを((*拡張モジュール*)) (extension module)といいます.わざわざ拡張モジュールを書く理由としては, 既存ライブラリの利用や速度向上などが挙げられます. 筆者はRubyを愛用していますが,Rubyの拡張モジュール作成は容易です.詳し いやり方はRubyに同梱されている(('README.EXT.jp'))に書かれてるので,こ こではより手っ取り早く入門することを目指し,本稿では実用的で小さな例と してKAKASIをRubyから使う拡張モジュールRuby/KAKASIを取り上げます. == KAKASIとは KAKASIは漢字かな混じり文をひらがなやローマ字に変換することを目的として 高橋裕信さんによって作成されたプログラムと辞書の総称であり,GPLで配布 されています((<[KP]>)).KAKASIの本体は(('kakasi(1)'))というコマンドで す. |% cat foo.txt |窓の外を誰かが歩いている |% kakasi -s -JH < foo.txt |まど の そと を だれか が あるい ている |% kakasi -s -U -Ja -Ha < foo.txt |MADO no SOTO wo DAREKA ga ARUI teiru == なぜ拡張モジュールなのか 筆者がKAKASIをRubyから利用するにあたって当初は(({fork}))した (('kakasi(1)'))とパイプを使ってプロセス間通信するという方法を取ろうと しましたが,(('kakasi(1)'))が出力する長さは事前には分からないため,こ の方法ではかなり面倒なことに気づきました.(('kakasi(1)'))の内部に立ち 入るほうがはるかに楽なので,拡張モジュールの作成を決めました. 幸い,最近のKAKASIを導入すると(('libkakasi.a'))が同時にインストールさ れます.libkakasiのドキュメント(('README.lib'))によれば,次の3つの関数 が提供されています. * (({int kakasi_getopt_argv(int, char **)})) * (({char *kakasi_do(char *)})) * (({int kakasi_close_kanwadict()})) (({kakasi_getopt_argv()}))は文字列配列を渡すと(('kakasi(1)'))の引数と して解析し,KAKASIを初期化します.(({kakasi_do()}))は渡された文字列を 処理し,((*内部で(({malloc()}))した文字列に代入してを返します(注意)*)). (({kakasi_close_kanwadict()}))は辞書を閉じ,再度 (({kakasi_getopt_argv()}))で初期化できる状態にします. == RubyのGC RubyもGC(garbage collection -- ゴミ集め)を採用しています.GCとは使われ なくなった領域を自動的に解放する仕組みのことです.GCの対象にするにはオ ブジェクトを生成する際に領域を解放する関数をGCに教えてやらなければなり ません.組み込みのクラスに関しては用意された生成用の関数がこの辺のこと をやってくれるのですが,いずれにせよユーザが単に(({malloc(3)}))しただ けでは,使われなくなってもゴミとして回収してくれないのでユーザの責任で 解放する必要があります. libkakasi の(({kakasi_do()}))は内部で(({malloc()}))して文字列を返すた め,(({kakasi_do()}))の返すポインタが指す領域は,こちらで(({free()})) してやらなければなりません.今回の作成において注意しなければならない点 はこれだけです. == 仕様 (({Kakasi}))というモジュールを設け,そのモジュール関数として, (({Kakasi::kakasi(((|opt|)), ((|src|)))}))を作ることにしました. ((|opt|))はオプションをあらわす文字列で,((|src|))はKAKASIに処理される 文字列で,結果を値として返します.KAKASIの初期化等は必要に応じて内部で 面倒みます. ちなみに(({Kakasi}))モジュールのユーザは次のようなコードを補うことで (({Kakasi::kakasi}))を1引数をとる文字列のメソッドにすることが出来ます. |require "kakasi" | |class String | def kakasi(opt) | Kakasi::kakasi(opt,self) | end |end このように組み込みクラスにメソッドを追加できる動的な性質はRubyのうれし い点のひとつです. == 実装と注意点 先にRubyの拡張モジュールの呼び出される仕組みについて説明しておきます. 拡張モジュールはRubyで書かれたライブラリと同様に(({require}))でロード します.たとえば, |require "kakasi" ならば,ライブラリのあるディレクトリ(({$:}))が順に検索され, (('kakasi.rb')) もしくは (('kakasi.so'))(Win32なら(('kakasi.dll')))が みつかったらロードされます.拡張子が(('.so'))(または(('.dll')))ならば, そのライブラリの関数(({Init_kakasi()}))がまず呼び出され初期化されます. 拡張モジュールの初期化の主な仕事は,Cの関数名とメソッド名の結びつけと, クラスやモジュール,定数の定義などです.またこの関数のように初期化関数 の名前のアンダースコア「(('_'))」以降はファイルのベースネームと一致し ていなければなりません. なお,初期化関数を含むファイルの名前を((*(('拡張モジュール名.c'))*))に しておくと(('Makefile'))の作成が非常に簡単になります. 以下にKakasi拡張モジュールの実装である(('kakasi.c'))を掲げます. || 0 /* || 1 * kakasi.c -- Kakasi module || 2 * Copyright (C) 1999,2000 GOTO Kentaro || 3 */ || 4 || 5 #include || 6 #include "ruby.h" || 7 #include "libkakasi.h" || 8 || 9 #define OPTMAX 1024 ||10 #define min(x,y) ((x)<(y) ? (x) : (y)) ||11 ||12 static int dic_closed = 1, len = 0; ||13 static char prev_opt_ptr[OPTMAX]; ||14 ||15 static VALUE ||16 rb_kakasi_kakasi(obj, opt, src) ||17 VALUE obj, opt, src; ||18 { ||19 int argc = 0, i = 0; ||20 char **argv, **opts; ||21 char *buf, *opt_ptr, *t; ||22 VALUE dst; ||23 ||24 Check_Type(src, T_STRING); ||25 ||26 /* return "" immediately if source str is empty */ ||27 if (RSTRING(src)->len == 0) ||28 return rb_str_new2(""); ||29 ||30 Check_Type(opt, T_STRING); ||31 ||32 /* initialize kakasi iff opt != previous opt */ ||33 if (0 == len || 0 != strncmp(RSTRING(opt)->ptr, prev_opt_ptr, ||34 min(RSTRING(opt)->len, len))) { ||35 strncpy(prev_opt_ptr, RSTRING(opt)->ptr, RSTRING(opt)->len); ||36 len = RSTRING(opt)->len; ||37 ||38 if (len + 1 > OPTMAX) { ||39 rb_raise(rb_eArgError, "too long 1st arg (should be < 1023)"); ||40 } ||41 ||42 if (!dic_closed) { ||43 kakasi_close_kanwadict(); ||44 dic_closed = 1; ||45 } ||46 ||47 argv = opts = ALLOCA_N(char*, RSTRING(opt)->len); ||48 *opts++ = "kakasi"; ||49 argc++; ||50 ||51 opt_ptr = ALLOCA_N(char, 1 + RSTRING(opt)->len); ||52 strncpy(opt_ptr, RSTRING(opt)->ptr, RSTRING(opt)->len); ||53 opt_ptr[RSTRING(opt)->len] = '\0'; ||54 ||55 if (*opts++ = strtok(opt_ptr, " \t")) { ||56 argc++; ||57 while (t = strtok(NULL, " \t")) { ||58 *opts++ = t; ||59 argc++; ||60 } ||61 } ||62 ||63 if (0 != kakasi_getopt_argv(argc, argv)) ||64 rb_raise(rb_eRuntimeError, "failed to initialize kakasi"); ||65 dic_closed = 0; ||66 } ||67 ||68 dst = rb_str_new2(""); ||69 while (i < RSTRING(src)->len) { ||70 if (*(RSTRING(src)->ptr + i) != '\0') { ||71 buf = kakasi_do((RSTRING(src)->ptr + i)); ||72 rb_str_concat(dst, rb_str_new2(buf)); ||73 free(buf); ||74 while (*(RSTRING(src)->ptr + i) != '\0') { ||75 i++; ||76 } ||77 } ||78 if (i == RSTRING(src)->len) { ||79 break; ||80 } ||81 rb_str_concat(dst, rb_str_new("\0", 1)); ||82 i++; ||83 } ||84 ||85 return dst; ||86 } ||87 ||88 void ||89 Init_kakasi() ||90 { ||91 VALUE mKakasi = rb_define_module("Kakasi"); ||92 ||93 rb_define_module_function(mKakasi, "kakasi", rb_kakasi_kakasi, 2); ||94 rb_define_const(mKakasi, "KAKASI_VERSION", rb_str_new2("2000-01-23")); ||95 } このプログラムは大きくわけて, (1) ヘッダファイルのインクルードとマクロ(5-10行) (2) static変数の定義(12-13行) (3) (({rb_kakasi_kakasi()}))の定義(15-86行) (4) 拡張モジュール初期化関数(88-95行) の4つの部分からなります.先に初期化関数からみておきます. === 拡張モジュールの初期化関数(88-95行) 91行目ではモジュール(({Kakasi}))を定義しています.引数はモジュール名を あらわす文字列です. 93行目で,(({rb_define_module_function()}))で2引数のモジュール関数 (({Kakasi::kakasi}))を定義しています.第4引数は,Cの関数の第2引数以降 の形式をあらわし,非負のときは引数の個数をあらわします. 94行目は,定数(({KAKASI_VERSION}))を(({rb_define_const()}))で定義して います.第3引数に現れる(({rb_str_new2()}))については後で述べます. 以下最初から順にポイントを示します. === ヘッダファイルとマクロ定義部 6,7行目の(('ruby.h')),(('libkakasi.h'))はそれぞれRubyの C API と libkakasiを使うために必要です.9行目の(({OPTMAX}))は (({kakasi_getopt_argv()}))に渡す文字列の長さの最大値です. === static変数の定義部 12行目と13行目で定義したstatic変数は以下のような高速化のためです.上で 定めた仕様では,必要に応じて(({kakasi_getopt_argv()}))を呼び出すことに していますが,呼び出しの必要性は,(({Kakasi::kakasi}))に渡された第1引 数が前回の呼び出しと異なることで判定します.そのために,前回の呼び出し の際の引数を覚えておく必要があります.13行目の(({prev_opt_ptr}))はその ための配列で,12行目の(({len}))はその長さを表します.また辞書が開いて いるかどうかを表す変数として(({dic_closed}))を用意しています. === rb_kakasi_kakasi()の定義 ==== 宣言 15行目からが(({Kakasi::kakasi}))の実装である(({rb_kakasi_kakasi()}))で す.この関数の型のようにRubyのオブジェクトはCの世界ではVALUEという型を 持ちます.Rubyのメソッドに対応するCの関数は,第1引数に(({self}))を受け るための引数が来ます.よって,この関数が呼ばれると(({obj}))にはRubyの (({self}))が代入されますが,ここでは使いません. 残りの引数は93行目で指定した通り,2個のメソッドの引数(({opt}))と (({src}))です.これらの引数もRubyのオブジェクトなので型は(({VALUE}))で す. (({VALUE}))は実際は構造体へのポインタで,Cのデータと橋渡しするためのた めにいろいろなマクロが提供されています.24行目では(({n}))のクラスを検 査するマクロ(({Check_Type}))を使って,文字列クラスをあらわすデータタイ プ(({T_STRING}))であるかを判定しています.実行時にデータタイプが合わな ければ例外(({TypeError}))が発生します. ==== RubyとCのデータのやりとり 27行目で(({src}))が空文字列かどうかを調べます.(({VALUE}))型のデータ は適切な型にキャストしてやらないと,実体であるCの構造体にアクセス出来 ません.このキャストは対象が文字列ならば(({RSTRING()}))というマクロが やってくれます.Rubyの文字列は(({len}))というメンバにその長さが入って いますので,これが(({0}))かどうかをみればよいわけです.また(({ptr}))と いうメンバにはCの文字列へのポインタが入っています. 逆にCの文字列からRubyの文字列を作るにはいくつかの関数がありますが,こ こでは(({rb_str_new2()}))を使っています(28行目). ==== Cの文字列とRubyの文字列 47-66行目では(({opt}))を(({kakasi_getopt_argv()}))に渡すため文字列配列 (({argv}))に分解します.まず,47行目で領域を確保しています.マクロ (({ALLOCA_N()}))は第1引数で指定された型の配列を第2引数の値だけ (({alloca(3)}))を使って確保します. 文字列配列への分解は(({strtok(3)}))を使うことにしました. (({strtok()}))は引数を書き換えながら走査するので,壊されても構わないよ うに51-53行目で(({opt_ptr}))にコピーして渡しています. 53行目でnul文字(({'\0'}))を追加しているのを理解するにはRubyの文字列の 性質を知る必要があるでしょう.Rubyの文字列はCの文字列と違い,nulを含め ることが出来ます.逆にいえば,nulが文字列の終端を意味しません. (({strtok()}))はそんなことを知らないので,ちゃんとnulで終わらせてから 渡す必要があるのです.ちなみに,このようなCの文字列との違いはPerlや Pythonでも同様です. ==== KAKASIによる変換 68-81行目が実際の(({kakasi_do()}))による変換です.結果は(({dst}))に追 加していきます.(({src}))がヌルを含んでいた場合を考慮してやや複雑です が,((*キモは73行目*))で,この(({buf}))の指す領域は(({kakasi_do()}))内 で(({malloc()}))されたモノなので(({free()}))しています. === コンパイル Rubyでは次のような(({'extconf.rb'}))を実行すれば,Rubyのインストール状 況に合った(({Makefile}))ファイルが作成されます. |% cat exconf.rb |require "mkmf" | |$CFLAGS += " -I /usr/local/include " |$LOCAL_LIBS += " -L /usr/local/lib -lkakasi " |create_makefile("kakasi") |% ruby extconf.rb これを使って make すると(('kakasi.so'))のような名前のRuby用の実行時ラ イブラリが出来ます. さっそく使ってみましょう.まずは動作確認のために漢字とカタカナをローマ 字に読み下してみます(((%kakasi -Ja -Ka%))相当). |% cat testkakasi.rb |require "kakasi" |puts Kakasi::kakasi("-Ja -Ka", "ドッペル玄関") |% ruby testkakasi.rb |dopperugenkan 確かに(('"ドッペル玄関"'))が(('dopperugenkan'))と読み下されています. めでたしめでたし. (('kakasi-000123.tar.gz'))に含まれるプログラム(('wdcnt'))は引数として 与えられたテキストファイルをKAKASIのわかち書き機能で単語に分解し,頻度 順に出力するプログラムです.出力は「頻度 # 単語」という形になっている ので直接 gnuplot でプロットできます.試しに国民生活白書((<[WP]>))の単 語を数えて横軸を順位,縦軸を頻度にとった両対数グラフにしました(図1). 頻度は出現数をのべ単語数で割った相対頻度です.Zipfの法則((<[Man]>))と 比較するため,順位の0.85乗に逆比例する関数のグラフを併記しています. <<< wp あとは((%make install%))で,拡張モジュールが適切なディレクトリにインス トールされます. == フィードバック この拡張モジュールは1999年の7月に最初の版を公開しました((<[Got]>)).当 初は(({opt}))や(({src}))がnulを含む場合のことを考慮していなかったため, 不具合がユーザからruby-ext((<[RE]>))で指摘されました.意見を取り込み, 現在の形になりました. また,このままで問題がないわけではありません.たとえば,日本語で終わる 文字列をJISで出力したとき,終わりの状態がJISのままになってしまうという 問題があります. |p Kakasi::kakasi("-ojis", "かな") | #=> "\e$B$+$J" しかし,これは拡張モジュールのバグというよりもKAKASIの(仕様の)バグとも 呼べるようなモノなので,現在KAKASIの修正を提案中です.この号が出ている ころにはこの点を改善した新しいKAKASIが出ているかも知れません. == REFERENCES :[MI] まつもとゆきひろ, 石塚圭樹, ``オブジェクト指向スクリプト言語Ruby'', アスキー出版局 :[KP] KAKASI Project, ``KAKASI - 漢字→かな(ローマ字)変換プログラム'', (()) :[Got] Gotoken, ``Ruby/Kakasi'', (()) :[WP] 経済企画庁, ``平成11年度国民生活白書'', (()) :[Man] ベンワー. B. マンデルブロー, ``フラクタル幾何学'', 広中平祐監訳, 日経サイエンス社. :[RE] ruby-extメーリングリスト, (()) =end