Gauche:MeCab
MeCabの Gaucheバインディングを作っていく様子を実況公開するコーナー。-- fuyuki
普通にやれば1日仕事と思いますが、説明を加えながら時間をかけてのんびりやります。
製作の過程はCVSに残していきます。
第1の日
能書き
そもそもバインディングとはなんぞやということがまず初心者にはわからない と思う。「束縛」と日本語にしてみたところで状況が好転しないのは 名前束縛の場合といっしょ。 ここは「Cライブラリへのインターフェース」とでも言っておいたほうがわか りがいいと思う。
たとえばGaucheにはGauche-gtkというバインディングがあって、これを介せば GaucheからGTK+の機能を使うことができる。 Fixpointみたいなソフトウェ アがさくさく作れてしまうわけだ。
Gauche <----> Gauche-gtk <----> GTK+
世の中にはすでにイヤというほどCライブラリがあるわけで、Gaucheの側から すればちょっとしたインターフェース(つまりバインディング。まあ Gauche-gtk の場合は「ちょっとした」なんて程度ではすまされないんだけど) を書くだけでその機能を利用できるというのは大変においしい話だ。
どうやんの?
Gaucheではstubと呼ばれるSchemeとCの混ざったようなファイルを書く。ある いは(手間を惜しまないのであれば)全部Cで書いてもかまわない。SWIG とか FFIとかは当面Gaucheには関係ないので忘れてていい。
下準備
ふつうならバインディングを作ろうというライブラリの マニュアルをま じめに読んで計画を立てたりするんだろうが、筆者はそんなことはしない。い きなり書き始める。どんな機能があるライブラリなのかーとかは作ってくうち にわかってくるからよしとする。まあ最低限ライセンスがやばくないことと、 他言語用のバインディングがすでにあってなんか安心かなーくらいは見ておく。
テンプレート
Gaucheの配布物には spigot という拡張モジュールのサンプルがついているので、こいつをテンプ レートとして使うことができる。とはいえ、spigotはなんかのライブラリへの バインディングではなく、自前で実装した機能(πを計算するのだ)をGauche側 に見せる拡張モジュールだから、バインディングの場合とはちょっと雰囲気が 違うかもしれない。
自前テンプレート
ということで、ここでは私家版のテンプレートでいくことにする。まずは何は ともあれGauche-mecabというディレクトリを掘って、その中に必要なファイル をぼこぼこ置いていく。
Gaucheはあんまり自分用に特化したツールを作らず、デファクトスタンダード になっているものに乗っかる方針でやっているので、バインディングを作ると きはやっぱりautotools(というかほぼautoconf限定)のお世話になる。 で、まずはこの3ファイルが必要になるのでどっかからコピーしてくる。 automake に入ってるのが最新なんじゃないかな。
config.guess config.sub install-sh
configureスクリプトを作るためのスクリプトはautogen.shという名前で用意 しておくことにしている。中身はすごく単純。
aclocal autoconf
configure.acはこんなの。"AC_GAUCHE"で始まるマクロはただのおまじないと 思ってかまわない。筆者もたいして理解してない。
AC_PREREQ(2.54) AC_INIT(Gauche-mecab, 0.0) AC_CONFIG_SRCDIR(mecab.stub) AC_CANONICAL_SYSTEM AC_GAUCHE_INIT_EXT AC_GAUCHE_INSTALL_TYPE(site) AC_GAUCHE_CC AC_GAUCHE_FLAGS AC_PROG_INSTALL AC_GAUCHE_FIX_LIBS echo $PACKAGE_VERSION > VERSION AC_OUTPUT(Makefile)
やっぱりMakefile.inを書かなきゃならない。めんどい。まあMeCabについて言 えばmecab-configから必要な値を持ってこられるぶん楽なんだけど。
SHELL = @SHELL@ prefix = @prefix@ exec_prefix = @exec_prefix@ bindir = @bindir@ libdir = @libdir@ VPATH = $(srcdir) # These may be overridden by make invocators DESTDIR = OPTFLAGS = @OPTFLAGS@ CC = @CC@ LIBS = @LIBS@ `mecab-config --libs` CFLAGS = @CFLAGS@ `mecab-config --cflags` LDFLAGS = @LDFLAGS@ INSTALL = @INSTALL@ INSTALL_TYPE = @INSTALL_TYPE@ # Set by configure GOSH = @GOSH@ GAUCHE_CONFIG = @GAUCHE_CONFIG@ OBJEXT = @OBJEXT@ DSOEXT = @SOEXT@ MODULE = mecab ARCHFILES = $(MODULE).$(DSOEXT) SCMFILES = $(MODULE).scm HEADERS = TARGET = $(ARCHFILES) OBJS = $(MODULE)_head.$(OBJEXT) ? mecab.$(OBJEXT) ? $(MODULE)_tail.$(OBJEXT) GENERATED = mecab.c $(MODULE)_head.c $(MODULE)_tail.c CONFIG_GENERATED = Makefile config.cache config.log config.status ? configure.lineno autom4te*.cache HEADER_INSTALL_DIR = $(DESTDIR)`$(GAUCHE_CONFIG) --$(INSTALL_TYPE)incdir` SCM_INSTALL_DIR = $(DESTDIR)`$(GAUCHE_CONFIG) --$(INSTALL_TYPE)libdir` ARCH_INSTALL_DIR = $(DESTDIR)`$(GAUCHE_CONFIG) --$(INSTALL_TYPE)archdir` all : $(TARGET) .SUFFIXES: .o .stub .c.o: $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@ .stub.c: $(GOSH) genstub $< $(MODULE).$(DSOEXT) : $(OBJS) $(CC) -Wl,-rpath -Wl,`gauche-config --sysarchdir` $(LDFLAGS) $(MODULE).$(DSOEXT) $(OBJS) $(LIBS) $(MODULE)_head.c $(MODULE)_tail.c : $(GAUCHE_CONFIG) --fixup-extension $(MODULE) check : all @rm -f test.log $(GOSH) -I. test.scm > test.log install : all if test ! -z "$(HEADERS)"; then ? $(INSTALL) -d $(HEADER_INSTALL_DIR); ? $(INSTALL) -m 444 $(HEADERS) $(HEADER_INSTALL_DIR); ? fi if test ! -z "$(SCMFILES)"; then ? $(INSTALL) -d $(SCM_INSTALL_DIR); ? $(INSTALL) -m 444 $(SCMFILES) $(SCM_INSTALL_DIR); ? fi if test ! -z "$(ARCHFILES)"; then ? $(INSTALL) -d $(ARCH_INSTALL_DIR); ? $(INSTALL) -m 555 $(ARCHFILES) $(ARCH_INSTALL_DIR); ? fi clean : rm -rf core $(TARGET) $(OBJS) $(GENERATED) *~ test.log so_locations distclean : clean rm -rf $(CONFIG_GENERATED) maintainer-clean : clean rm -rf $(CONFIG_GENERATED) configure
stubファイルはとりあえずこれだけ。意味はあとで説明するから今はわからな くてもかまわない。以下のファイルも同様。
" #include <gauche/extend.h> " ;; Local variables: ;; mode: scheme ;; end:
Gaucheからuseしたときにロードされるmecab.scmはこんなやつ。
(define-module mecab) (select-module mecab) (dynamic-load "mecab") (provide "mecab")
Gaucheはテストファースト(cf. Scheme:テストファースト)とまではいか ないが、きちんとテストケースを用意することを重視している。ので、テンプ レートにもちゃんとtest.scmを入れてある。
(use gauche.test) (test-start "mecab") (if (member "." *load-path*) ;; trick to allow in-place test (load "mecab")) (import mecab) (test-module 'mecab) (test-end)
動くかな?
実はこれだけ用意しておけば、ちゃんとmake checkが通るところまで確認で きるのだ。
$ ./autogen.sh $ ./configure checking build system type... i386-unknown-freebsd5.2.1 checking host system type... i386-unknown-freebsd5.2.1 checking target system type... i386-unknown-freebsd5.2.1 checking for gosh... /opt/bin/gosh checking for gauche-config... /opt/bin/gauche-config checking for gauche-package... /opt/bin/gauche-package checking for gauche-install... /opt/bin/gauche-install checking for a BSD-compatible install... /usr/bin/install -c configure: creating ./config.status config.status: creating Makefile
$ make /opt/bin/gauche-config --fixup-extension mecab gcc -g -O2 -I/opt/lib/gauche/0.8/include -I/usr/local/include `mecab-config -- cflags` -c mecab_head.c -o mecab_head.o /opt/bin/gosh genstub mecab.stub gcc -g -O2 -I/opt/lib/gauche/0.8/include -I/usr/local/include `mecab-config -- cflags` -c mecab.c -o mecab.o gcc -g -O2 -I/opt/lib/gauche/0.8/include -I/usr/local/include `mecab-config -- cflags` -c mecab_tail.c -o mecab_tail.o gcc -Wl,-rpath -Wl,`gauche-config --sysarchdir` -L/usr/local/lib -shared -o meca b.so mecab_head.o mecab.o mecab_tail.o -L/opt/lib/gauche/0.8/i386-unknown-free bsd5.2.1 -L/usr/local/lib -lgauche -lcrypt -lutil -lm `mecab-config --libs`
$ make check /opt/bin/gosh -I. test.scm > test.log Testing mecab ... passed.
「テストケースが空っぽなんだからあたりまえじゃん」という声が聞こえてき そうだが、これがpassするだけでもずいぶんいろんなものがまともに動いてい ることが確認できるのだ。この説明もあとにまわすとして、 今日はここまで。
ダウンロード
$ cvs -d:pserver:anonymous@cvs.sourceforge.jp:/cvsroot/gauche login Passwdrd: <リターン> $ cvs -d:pserver:anonymous@cvs.sourceforge.jp:/cvsroot/gauche co -r day1 Gauche-mecab
とすると Gauche-mecab というディレクトリができるので今日の内容を自分で試してみることができます。
質問・コメント
ナイスな企画ありがとうございます。 うちの環境だと autoconf -I `gauche-config --ac` とやらなくてはなりませんでした。autoconf-2.57 を使ってます。 --skimu
すばらしい。ところで、Gauche-0.8前提なら、INSTALLにgauche-installを使うと installターゲットがすっきり書けます。spigotのMakefile.inを見てください。 実はAC_PROG_INSTALLは不要になるので、install-shも排除できるかな? spigotの configure.inでは残しちゃってるけど。 それから、これもspigotを見てもらいたいのですが、AC_GAUCHE_MAKE_GPDという autoconfマクロが追加されています。これは、${module_name}.gpdという ファイルをトップディレクトリに作成します。それを適切なディレクトリに インストールしておけば、gauche-package list でパッケージがリストされたり、 gauche-package reconfigure でconfigureオプションが見られたりします。 (このへんは0.8にはドキュメントが間に合わなかったのですが、 このインタフェースは変わらないと思います)。 --Shiro
あ、あと、test.scmのtrick to allow in-place testの部分ってまだ必要ですか? これってどっかの時点で要らなくなったような気がしてたのだけれど。(use mecab)でどうでしょう。 --Shiro
初めまして。昔やっつけ仕事的に mecab_sparse_to_str に対するラッパーみたいなものを作ったのがハードディスクの底に眠っているのですが、何かお役に立てるでしょうか。 -- leque
あ、Gauche 0.8前提というのを書くの忘れてましたね。で、テンプレートの書き方もちょっと古かったと。次回はそのへん直すところからはじめましょうか。-- fuyuki
第2の日
修正
突然だがこの連載はGauche 0.8を前提にしている。そういうことに決まった。 それにしてはテンプレートの書き方が古かったのだが、何事もなかったかのよ うに修正を加えつつ話をつづけることにする。現在の動脈硬化を防ぐためには 過去はゆらぎつづけなければならない。それって歴史の改変ですか。
autogen.sh
といいつつautogen.shは変更なし。古いバージョンのGaucheでも動くようにす るには、skimu氏の言うように
autoconf -I `gauche-config --ac`
とするのがいちばん安全なのだが、筆者の趣味でaclocalを使うことにする。 ほかのautoconfマクロを取り込みたくなったときにこのほうが楽だし。
ちなみにGauche 0.8がインストールされているにもかかわらずautogen.shを実 行してもaclocal.m4が生成されない場合は、なんか環境がおかしなことになっ ている可能性が高い。まずは
$ aclocal --print-ac-dir
を実行して、表示されるパスにgauche.m4がいることを確認するべし。
configure.ac
configure.acはほとんどspigotの引き写し。
AC_PREREQ(2.54) AC_INIT(Gauche-mecab, 0.0) AC_CONFIG_SRCDIR(mecab.stub) AC_GAUCHE_INIT_EXT AC_GAUCHE_INSTALL_TYPE(site) AC_GAUCHE_CC AC_GAUCHE_FLAGS AC_GAUCHE_FIX_LIBS AC_GAUCHE_MAKE_GPD echo $PACKAGE_VERSION > VERSION AC_OUTPUT(Makefile)
悩むとすればAC_GAUCHE_INSTALL_TYPEくらいだろう。sysとsite の二通りが指 定できるのだが、どう使い分けたらいいのかよくわからない。筆者としては外 部モジュールは全部siteにしたほうがいいのではないかと思っているのだが、 Gauche-gtkやGauche-glはsysになっている。まあたいがいsiteでいいでしょ。
Makefile.in
これもほとんどs/spigot/mecab/しただけかも。とはいえ、cleanターゲットを cleanにするのはけっこう面倒だったりするんだけど。
SHELL = @SHELL@ prefix = @prefix@ exec_prefix = @exec_prefix@ bindir = @bindir@ libdir = @libdir@ VPATH = $(srcdir) # These may be overridden by make invocators DESTDIR = OPTFLAGS = @OPTFLAGS@ CC = @CC@ LIBS = @LIBS@ `mecab-config --libs` CFLAGS = @CFLAGS@ `mecab-config --cflags` LDFLAGS = @LDFLAGS@ INSTALL_TYPE = @INSTALL_TYPE@ # Set by configure GOSH = @GOSH@ GAUCHE_CONFIG = @GAUCHE_CONFIG@ OBJEXT = @OBJEXT@ DSOEXT = @SOEXT@ INSTALL = @GAUCHE_INSTALL@ MODULE = mecab ARCHFILES = $(MODULE).$(DSOEXT) SCMFILES = $(MODULE).scm HEADERS = TARGET = $(ARCHFILES) OBJS = $(MODULE)_head.$(OBJEXT) ? mecab.$(OBJEXT) ? $(MODULE)_tail.$(OBJEXT) GENERATED = mecab.c $(MODULE)_head.c $(MODULE)_tail.c CONFIG_GENERATED = Makefile config.cache config.log config.status ? configure.lineno autom4te*.cache Gauche-$(MODULE).gpd HEADER_INSTALL_DIR = $(DESTDIR)`$(GAUCHE_CONFIG) --$(INSTALL_TYPE)incdir` SCM_INSTALL_DIR = $(DESTDIR)`$(GAUCHE_CONFIG) --$(INSTALL_TYPE)libdir` ARCH_INSTALL_DIR = $(DESTDIR)`$(GAUCHE_CONFIG) --$(INSTALL_TYPE)archdir` all : $(TARGET) .SUFFIXES: .o .stub .c.o: $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@ .stub.c: $(GOSH) genstub $< $(MODULE).$(DSOEXT): $(OBJS) $(CC) -Wl,-rpath -Wl,`gauche-config --sysarchdir` $(LDFLAGS) $(MODULE).$(DSOEXT) $(OBJS) $(LIBS) $(MODULE)_head.c $(MODULE)_tail.c : $(GAUCHE_CONFIG) --fixup-extension $(MODULE) check : all @rm -f test.log $(GOSH) -I. test.scm > test.log install : all $(INSTALL) -m 444 -T $(HEADER_INSTALL_DIR) $(HEADERS) $(INSTALL) -m 444 -T $(SCM_INSTALL_DIR) $(SCMFILES) $(INSTALL) -m 555 -T $(ARCH_INSTALL_DIR) $(ARCHFILES) $(INSTALL) -m 444 -T $(SCM_INSTALL_DIR)/.packages Gauche-$(MODULE).gpd clean : rm -rf core $(TARGET) $(OBJS) $(GENERATED) *~ test.log so_locations distclean : clean rm -rf $(CONFIG_GENERATED) maintainer-clean : clean rm -rf $(CONFIG_GENERATED) VERSION aclocal.m4 configure
mecab.scm
ここまでのファイルはバインディングをビルドするために必要なごたごたであっ て、本題はようやくここから始まる。
前回はしょっちゃった部分をまず説明すると、バインディングは通常scmファ イルとDSOファイル(多くのシステムでは.soという拡張子が付く)のセットになっ ている。バインディングのうち、Gauche側へのインターフェースになるのが scmファイル、ライブラリ(この連載の場合はMeCab)側へのインターフェースに なるのがDSOという役割分担だ。
Gauche <----> (mecab.scm - mecab.so) <----> MeCab
DSO(Dynamic Shared Object)についての説明はリンクを張って逃げようと思っ たんだが、手頃なのが見つからない。しょうがないので簡単に説明しておくと、 要するにDSOとはプログラムの実行中に動的にロードできるオブジェクトのこ とだ。Gaucheのモジュールなら動的にロードできるのはあたりまえの話だが、 そういうのが古式ゆかしいオブジェクトファイル(.o)でも可能になったものと 思っておけばいい。Apacheのmod_*はこの仕組みを使っている。
で、mecab.scmに話を戻すと、scmファイルの仕事は主に二つある。一つはモ ジュールを定義すること。これはDSOを使わない一般的なモジュールのやりか たとまったく同じで、
(define-module mecab (use うんたら) (use かんたら) (export 外に見せたいものをずらずら並べる)) (select-module mecab) なんかモジュールの中身 シンプルなバインディングの場合はカラだったりもする (provide "mecab")
といった形をとる。
もう一つの大きな仕事はDSOをロードすることで、これは
(dynamic-load "mecab")
の一行だけ。こうするとmecab.so内で定義された関数やなんかがこのモジュー ルの中で使えるようになるし、exportしてユーザーに見せることも可能になる。
ちなみに上のファイル指定で.soのような拡張子がついていないのは、システ ムによってDSOの拡張子が違うからだ。そういった厄介な部分はGaucheが吸収 してくれているので感謝しつつ使うべし。
mecab.stub
でもってそのmecab.soのネタ元になるのがstubファイルだ。前回のファイルを もう一度見てみよう。
" #include <gauche/extend.h> " ;; Local variables: ;; mode: scheme ;; end:
下の3行はEmacsに「このファイルはschemeモードで編集ということでよろしく」 と頼んでいるだけなので、vi使いとかには関係ない。まあScheme なファイル なんだなーという目印にはなる。その上にCみたいなinclude行があるが、これ は実際Cのコードだ。
stubの基本その1は、
「ダブルクオートでくくられた部分(文字列)はそのままCに落ちる」
ということ。まずはこの流れを追ってみよう。
Makefileを見てもらえばわかるように、.soは
.stub -> .c -> .so
という順序で生成される。で、.stubを.cに変換するのがgenstubというプログ ラムだ。
$ gosh genstub
のように起動する。
ためしに
$ gosh genstub mecab.stub
とすると、次のようなファイルがmecab.cとして生成されるはずだ。
/* Generated by genstub. Do not edit. */ /* source: mecab.stub */ #include <gauche.h> #include <gauche/extend.h> void Scm_Init_mecab(ScmModule *module) { }
stub中のinclude文がそのまま現れていることがわかる。Scm_Init_mecab()と いう関数は、dynamic-loadされたときに初期化ルーチンとして呼ばれる。今の ところ空っぽだがそのうち中身が入ってくる。
「こんなの直にCを書いたっていいじゃん」というのはまったくその通り。で もそのうちstubのありがたみがわかってくるはずだ。めんどくさい定型的な処 理のかなりの部分はgenstubが自動的に生成してくれる。何をどんなふうにやっ てくれるかは次回のお楽しみ。とりあえず今回は、gauche/extend.hは拡張モジュー ルではほとんど常にincludeしなきゃならないんだよーというところまで。
test.scm
うーむ、今回もテストケースが増えなかった。まあいっか。
(use gauche.test) (test-start "mecab") (use mecab) (test-module 'mecab) (test-end)
でもcommitする前には必ずmake checkするのを忘れずに、と。最低限DSOが生 成されてロードできてることくらいは確認できるからね。
ついでにmakeしてできたDSOのリンク状況を確認しておくといいかもしれない。 手元ではこんな感じ。システムによってはそんな情報はDSOについてないかも しれないけど。
$ ldd ./mecab.so ./mecab.so: libgauche.so => /opt/lib/gauche/0.8/i386-unknown-freebsd5.2.1/libgauche.so (0x28148000) libcrypt.so.2 => /lib/libcrypt.so.2 (0x281f0000) libutil.so.4 => /lib/libutil.so.4 (0x28209000) libm.so.2 => /lib/libm.so.2 (0x28215000) <b style="color:black;background-color:#ffff66">libmecab.so</b>.0 => /usr/local/lib/<b style="color:black;background-color:#ffff66">libmecab.so</b>.0 (0x2822e000) libstdc++.so.4 => /usr/lib/libstdc++.so.4 (0x2827a000) libc_r.so.5 => /usr/lib/libc_r.so.5 (0x28334000)
ダウンロード
$ cvs -d:pserver:anonymous@cvs.sourceforge.jp:/cvsroot/gauche login Passwdrd: <リターン> $ cvs -d:pserver:anonymous@cvs.sourceforge.jp:/cvsroot/gauche co -r day2 Gauche-mecab
とすると Gauche-mecab というディレクトリができるので今日の内容を自分で試してみることができます。
なお、前回までにGauche-mecabというディレクトリが用意されていれば、その中で
$ cvs up -r day2
とするだけでいいはずです。
第3の日
創造の苦悩
創世記の神の仕事なら3日目には植物がいい感じに生え揃ったりしてるんだが、 この神は要領が悪いのでいまごろになってようやく肝心かなめの部分に 手をつけようとしている。 おまけにあいだに日が開きすぎているのでこれまで何をやってたんだか思い出すのに 苦労したりもしている。大丈夫か神。
autotools地獄
MacOS Xにはautoconfやautomakeが標準で付いているらしい(Gauche:MacOSX)。 どういう考えでそういう設計にしたのかな。 ただでさえautotoolsはバージョン間の微妙な違いではまることが多いのに、 わざわざ話をややこしくしているように見えるんだが。
まあそういった現実には対処しなくてはならないと。 とりあえずこんなもんでどうでしょ。
aclocal -I `gauche-config --ac`
あ、10.2以前では-Iのかわりに-lを使うとか書いてあるな。どうしよう。
いいや、そんな現実は無視。(←間違った態度)
API瞥見
気をとり直してMeCabのマニュアルを 読むことにする。通読するのがめんどうなので、頭から見ていって出たとこ勝負でやっていくことにする。
まず最初にあるのはmecabのインスタンスを生成するmecab_new()だが、 引数がargcとargvなんでこれはきっとバインディングには関係ないだろう。パス。
次にも似たような関数があるが、こっちもなんかよくわかんない引数を取ることになっている。 なんだろこれ。
mecab_t *mecab_new2 (char *arg)
サンプルにもこの関数の使い方は書いてないので、しょうがないからMeCabのソースを見ることにする。 いきなりかい。
bool Param::open (const char *arg, const Option *opts) { char str [1024]; strncpy (str, arg, 1024); char* ptr [64]; unsigned int size = 1; ptr[0] = PACKAGE; for (char *p = str; *p ; ) { while (isspace (*p)) *p++ = '?0'; if (*p == '?0') break; ptr[size++] = p; if (size == 64) break; while (*p && ! isspace (*p)) p++; } return open (size, ptr, opts); }
んー、きっとargvに入っているような引数 を一つの文字列の中にずらずら並べたやつが欲しいんだろう。違うかな。 まあやってみりゃわかるか。
いきなり説明モードに入ると、ここで書かんとしているのはCの関数をGauche に見せるためのラッパーである。でもってGaucheには低レベルバインディングでは なるべく元となっているCの構造をそのまま見せようという大ざっぱな方針がある。 それで使いにくければさらにSchemeでラッピングする。
このやり方には少なくとも一つの大きな利点がある。バインディングを作るときに あまり最初から考え込まなくてもいいということだ。 Cの関数をSchemeの関数に1:1対応させればいいんだからね。 ということでわりと無計画にわしわし進めていけるわけだ。
初期化
と、その前にめんどうなところをずいぶん通らなきゃならない。どん。
" #include <gauche/extend.h> #include <mecab.h> typedef struct ScmMeCabRec { SCM_HEADER; mecab_t *m; /* NULL if closed */ } ScmMeCab; SCM_CLASS_DECL(Scm_MeCabClass); #define SCM_CLASS_MECAB (&Scm_MeCabClass) #define SCM_MECAB(obj) ((ScmMeCab*)(obj)) #define SCM_MECABP(obj) (SCM_XTYPEP(obj, SCM_CLASS_MECAB)) /* Hack for initialization stub */ static void internal_init(ScmModule*); void Scm_Init_mecab(void) { ScmModule *mod; SCM_INIT_EXTENSION(mecab); mod = SCM_MODULE(SCM_FIND_MODULE(?"mecab?", TRUE)); internal_init(mod); } #define Scm_Init_mecab internal_init " (define-cclass <mecab> "ScmMeCab*" "Scm_MeCabClass" () ())
stubの中身がいきなりややこしくなった。ここでは
- モジュールの初期化
- mecab_t*を<mecab>というクラスのインスタンスのように見せる
という二つのことをやっている。
前者はHack for initialization stubというコメントのついたかたまりの中で やっている。これは本当に"hack"なのであんまり説明したくないのだが、しか たないから説明すると、ここではstub中にモジュールの初期化を埋め込むため のおまじないを唱えている。
どうしても何をやっているか知りたければ、stubから生成されるCのコードを 見るといい。
/* Hack for initialization stub */ static void internal_init(ScmModule*); void Scm_Init_mecab(void) { ScmModule *mod; SCM_INIT_EXTENSION(mecab); mod = SCM_MODULE(SCM_FIND_MODULE("mecab", TRUE)); internal_init(mod); } #define Scm_Init_mecab internal_init
void Scm_Init_mecab(ScmModule *module) { Scm_InitBuiltinClass(&Scm_MeCabClass, "<mecab>", NULL, TRUE, module); }
今のところは関数やマクロの名前からやっていることがおぼろげに想像できる 程度で十分だと思う。SCM_INIT_EXTENSIONはなんか初期化してるんだろうし、 SCM_FIND_MODULE でmecabモジュールを探してるんだろうし、 Scm_InitBuiltinClassでは<mecab> クラスをどうにかしてるんだろう。
深入りしたい人のために調べ方だけ書いておくと、なんかわからないものがあっ たらまずgauche.hを見ること。それで満足できなければ、Gaucheのソースをと ことん追いかけるしかない。今のところこのへんのまともなマニュアルはない。 GHG:に期待しよう。
クラス
Cをオブジェクト指向言語と呼ぶ人はあんまりいないと思うが、Cのライブラリ は意図的にOOっぽい構成をとっていることが多い。まあMeCabはC++なんだがそ れは置いとくとして、そういうもんだと無理に飲み込んでほしい。どういうこ とかというと、
- オブジェクトの生成(new)
- オブジェクトにメッセージを投げる(メソッド)
- オブジェクトの消去(delete)
のような枠組みがAPIから透けて見えるということだ。
といってもややこしい話では全然なくて、たとえばC標準のファイルポインタ (FILE*)だってオブジェクトみたいなもんだよねというだけの話。fopenがnew もどきで、fread やらfwrite がメソッドで、fcloseで死なすというような感 じ。
で、何が言いたいかというと、MeCabにおいてはmecab_t*がそのオブジェクト みたいなもので、これをScheme にマッピングするにはやはりGaucheのクラス 機構を使うのが自然だということだ。
要するにmecab_t*を<mecab>クラスのインスタンスのようにしたいわけだが、 それにはまたいろいろとめんどうな手続きを踏む必要がある。まずはこれから。
typedef struct ScmMeCabRec { SCM_HEADER; mecab_t *m; /* NULL if closed */ } ScmMeCab;
ここでは一つのmecab_tポインタをくるむだけのSchemeオブジェクトを宣言し ようとしている。SCM_HEADERというのは、たいがいのSchemeオブジェクトが持っ ていなければならない情報を埋め込むためのおまじないと思ってほしい。ペア でも文字列でもみんなこれを持っている。
SCM_CLASS_DECL(Scm_MeCabClass); #define SCM_CLASS_MECAB (&Scm_MeCabClass) #define SCM_MECAB(obj) ((ScmMeCab*)(obj)) #define SCM_MECABP(obj) (SCM_XTYPEP(obj, SCM_CLASS_MECAB))
SCM_CLASS_DECLはなんだか知らんがクラスの宣言のためのマクロ。次の3行で 定義しているのは、それぞれ
- <mecab>クラスを参照するためのマクロ
- 任意のSchemeオブジェクトを<mecab>オブジェクトへキャストするマクロ
- <mecab>オブジェクトかどうか調べるためのマクロ
このへんはクラスを定義するのに必然的に伴う義務のようなものだ。
最後になってしまったが、ようやくstubのstubらしい使い方をするところにた どり着いた。<mecab>クラスを定義しよう。
(define-cclass <mecab> "ScmMeCab*" "Scm_MeCabClass" () ())
これは次のようなCコードに展開される。
SCM_DEFINE_BUILTIN_CLASS(Scm_MeCabClass, NULL, NULL, NULL, NULL, SCM_CLASS_DEFAULT_CPL);
文字数があんまり変わんないような気もするが、それはここでは define-cclass のいちばん単純な使い方しかしていないからだ。もっとややこ しいクラスを定義する場合にはdefine-cclassは絶大な威力を発揮する。
ぼちぼち想像がついていると思うが、stubというのはこのようにScheme風に書 かれたフォームを自動的にCのコードに展開するためのものなのだ。何をどう 展開してくれるかというマニュアルはまだないが、 genstub のコメントを読めばだいたいのところはわかるようになっている。 GHG:genstubにもちょっと説明がある。
ちなみに、stubファイルの""の中で行の先頭にスペースを付けているのは、そうしない とEmacsの表示がおかしくなるからだ。他意はない。あと、「""の中ではどう やってインデントすればええのん?」という疑問を持つ人もいるだろうが、手 でやる。Emacsの自動インデントは効かない。それがいやなら、別に.cや.hを 作ってそっちにコードを移動してもいい、最終的につじつまが合いさえすれば、 複数ファイルに分かれることにはなんの問題もない。
筆者は単純なバインディングの場合には単一のstub に全部押し込んでしまう ことが多いが、このへんは趣味の問題。複数ファイルに割ったほうが少なくと も初期化のところはきれいに書ける。
不安
--enable-threads=pthreadsしたgoshじゃないと動作がおかしくなる場合があ ることに気づいた。やっぱりそういうもんなのかなあ。
ダウンロード
$ cvs -d:pserver:anonymous@cvs.sourceforge.jp:/cvsroot/gauche login Password: <リターン> $ cvs -d:pserver:anonymous@cvs.sourceforge.jp:/cvsroot/gauche co -r day3 Gauche-mecab
とすると Gauche-mecab というディレクトリができるので今日の内容を自分で試してみることができます。
なお、前回までにGauche-mecabというディレクトリが用意されていれば、その中で
$ cvs up -r day3
とするだけでいいはずです。
第4の日
中央集権の愛、あるいは構文的な予兆
へっぽこチェリストだった ゴーシュ は4日間のひみつ特訓の末にコンサートで満場の喝采を浴びるまでに成長する。 という理解にまあ間違いはないのだけれども、この話はむしろイニシエーション、 死と再生の儀礼の物語と受け取ったほうがいいのではなかろうか。
だいたいが童話だと思って読んでいるから猫やねずみが水車小屋の扉を叩いても 許されるんであって、実際にそんな現実把握をしていたら救急車が横付けになり 白い服を着た小さな男たちがわらわらと飛び出してくるに決まっているのだ。
ゴーシュの試練は4日で終わるわけではない。コンサートが行われたのはねず みの親子が去ってから6日目の晩のことなのだ。この間の語られなかった夜々 が筆者には気になって仕方がない。何があったかは想像するよりほかないが、 おそらくゴーシュは正気と狂気の境をさまよったのではあるまいか。
最後の夜に扉を叩いたのは誰だろう。それはゴーシュその人であっ たと筆者はどうしても思いたい。最後の敵/味方は常に自分自身にほか ならない。
コンサートを翌日に控え、ゴーシュはおのれ自身を聴衆にしてどんな演奏を繰 り広げたのだろうか。
生/死
あ、上は無視していいです。ただの伏線ですから。(嘘)
閑話休題。前回までに何をやったかというと、ようやく<mecab>というクラス が出来たところまで。今回はようやくインスタンスの生成/消滅に取りかかれ る。ここまでいったい何週間かかってるんだ。余計な話ばっかりしてるから悪 いのか。
autotools地獄(2)
gauche.m4はaclocal 1.8では取り込めないみたい。なんで?
インスタンスの生成
前回ちらっと説明したように、mecabインスタンスの生成にはmecab_new()もし くはmecab_new2()という関数を使う。ここではmecab_new2()をGaucheから使え るようにしてみよう。このへんからボチボチ何をやっているコードなのか理解 しないとヤバい感じになってくるので、ちっと気合を入れ直してほしい。
(define-cproc mecab-new2 (arg::<string>) " ScmMeCab *m = SCM_NEW(ScmMeCab); SCM_SET_CLASS(m, SCM_CLASS_MECAB); m->m = mecab_new2(SCM_STRING_START(arg)); SCM_RETURN(SCM_OBJ(m));")
define-cprocで始まるフォームはgenstubがCの関数に展開し、初期化処理など も自動的に生成してくれる。
mecab-new2はGaucheに見せる関数名。だからアンダースコアではなくハイフン を名前区切りに使っている。
arg::<string>というのは見た目で想像がつくように、mecab-new2は <string> 型の引数argを取りますよーという意味。Schemeの引数に型なんてあっ たっけ? と思うかもしれないが、まあ引数チェックをすることでそれっぽく 見せているということだ。
その次の4行に渡る文字列はそのままCのコードになる。
というような説明をずらずら並べるより、生成されたCコードを実際に見ていっ たほうが早いかもしれない。
static ScmObj mecab_mecab_new2(ScmObj *SCM_FP, int SCM_ARGCNT, void *data_) { ScmObj arg_scm; ScmString* arg; SCM_ENTER_SUBR("mecab-new2"); arg_scm = SCM_ARGREF(0); if (!SCM_STRINGP(arg_scm)) Scm_Error("string required, but got %S", arg_scm); arg = SCM_STRING(arg_scm); { ScmMeCab *m = SCM_NEW(ScmMeCab); SCM_SET_CLASS(m, SCM_CLASS_MECAB); m->m = mecab_new2(SCM_STRING_START(arg)); SCM_RETURN(SCM_OBJ(m)); } }
この
if (!SCM_STRINGP(arg_scm)) Scm_Error("string required, but got %S", arg_scm);
のところで引数の型チェックをやっているのは想像がつくと思う。こういう 「なくてはならない」けど「いちいち手で書くのはめんどくさい」あたりを勝 手にやってくれるのがstubのありがたいところだ。
型にあんまりこだわらないSchemeの世界になじんでいると、型チェックが「な くてはならない」というのがちょっと不思議に思えるかもしれないが、これは バインディングがCの世界に半身以上突っ込んでいるからだ。まじめにチェッ クしないとすぐにSEGVをくらったりして大変なことになる。
stubで使える型はGaucheの型(クラス)とイコールではないことにも注意してほ しい。これは単にCの世界とGaucheの世界を取り持つための便宜的なものだ。 どれがどれとどう対応するかは genstub のType handlingのところに書いてある。 よく使うのはこんなところ。
stub type | Scheme | C | Notes |
<fixnum> | <integer> | int | Integers within fixnum range |
<int> | <integer> | int | Integers representable in C |
<list> | <list> | ScmObj | |
<string> | <string> | ScmString* |
これを見て「<fixnum>と<int>ってどう違うの?」と思わない人は稀だと思う が、実は筆者にもよくわからない。Gauche(のfixnum)を中心に考えるか、C(の int)を中心に考えるかの差なのかなあ。
わりとどうでもいい話だが、define-cprocの""の最初にスペースを二つ開け ているのは、そうしておくとCに展開されたときにインデントが揃うからだ。 それだけ。
インスタンスの生成(2)
次にmecab-new2の中身を順に見ていこう。
ScmMeCab *m = SCM_NEW(ScmMeCab);
SCM_NEWは新しいSchemeオブジェクトを生成(というか正確には領域の確保だけ) するためのマクロ。引数としてはC の型を取る。ScmMeCab というのがどこで 出てきたかもう忘れてると思うけど、こんなやつ。
typedef struct ScmMeCabRec { SCM_HEADER; mecab_t *m; /* NULL if closed */ } ScmMeCab;
続く
SCM_SET_CLASS(m, SCM_CLASS_MECAB);
で、生まれたオブジェクトに「あんたは誰それの子なんだよ」と教えてあげる。 つまりmはSCM_CLASS_MECABのインスタンスなわけで。
ここまででおおよそ空っぽのmecabインスタンスが出来た雰囲気。さらに
m->m = mecab_new2(SCM_STRING_START(arg));
とすることで、実際に(MeCab的に)インスタンスが生成される、はず。そう MeCabのマニュアルに書いてあるから。
SCM_STRING_START(arg)は、<string>であるargに格納された文字列実体の先頭 ポインタを得ている。まあややこしいことは言わず、<string>をchar*に変換 するマクロだと思ってしまってもいいかもしれない。
m->mのところで混乱しないように。最初のmはScmMeCab*で、その構造体中に次 のmであるmecab_t*が含まれているという形。
関数から値を返すには、SCM_RETURNを使う。
SCM_RETURN(SCM_OBJ(m));
返すのは常にScmObj型でなければならないので、SCM_OBJでキャストしている。 ScmObjとScmMeCab*は一般/特殊の関係にある。Schemeオブジェクトなら何でも 入るのがScmObj, <mecab>インスタンスしか入らないのがScmMeCab*といった感 じ。
インスタンスの消滅
作ったオブジェクトはいつかは消さなければならない。筆者の趣味で、そのへ んを先に書いてしまうことにする。そういうAPIがきっとあるから。
MeCabの マニュアル をず ずーっと下にスクロールしてそれらしき関数をさがすと、mecab_destroy()と いうのがある。たぶんこれでしょ。
(define-cproc mecab-destroy (m::<mecab>) " mecab_destroy(m->m); m->m = NULL; SCM_RETURN(SCM_UNDEFINED);")
m::<mecab>というのがまず気になるところだろう。さっきこの::以降は型チェッ クのための情報だと説明したが、今回の<mecab>はこのstub内で始めて登場し たクラスだ。ちゃんとチェックコードが生成されるだろうか?
m_scm = SCM_ARGREF(0); if (!SCM_MECABP(m_scm)) Scm_Error("<mecab> required, but got %S", m_scm); m = SCM_MECAB(m_scm);
うまいもんですねえ。以前わけもわからず定義した(よね、たぶん)マクロはこ んなところで生かされるのでした。
mecab_destroy(m->m);
はまあいいとして、
m->m = NULL;
は、destroyされたかどうかのマーカーにm==NULLを使おうという魂胆。本当に それでいいかどうかは実はまだよくわかっていない。
こいつは値を返す必要がないんだけど、そういう場合はSchemeのお約束で <b style="color:black;background-color:#a0ffff">undefined</b>を返す。
SCM_RETURN(SCM_UNDEFINED);
生/死(2)
上のところでm==NULLのときはmecabインスタンスがdestroyされたというお約 束にしようという話をした。そのチェックのための関数も作っておく。
(define-cproc mecab-destroyed? (m::<mecab>) " SCM_RETURN(SCM_MAKE_BOOL(m->m == NULL));")
SCM_MAKE_BOOLは、Cの0/1を<boolean>に変換してくれる感じ。関数名に"?"を つけているのは、Schemeの関数らしくしましょうということで。このへんの呼吸は Gaucheのソースに含まれている*.stubを見てつかんでほしい。
そのとき注意しなければならないのが、ソース内では常に最新の推奨される書 き方がされているとは限らないということ。まあけもの道を歩いているつもりでい ればいいんじゃないでしょうか。(なげやり)
あと、熊とたたかうのはやめようね。負けるから。
テスト
あー、やっとテストが書けたよ。
(define m (mecab-new2 "")) (test* "mecab-new2" #t (is-a? m <mecab>)) (test* "mecab-destroy" #f (mecab-destroyed? m)) (mecab-destroy m) (test* "mecab-destroy" #t (mecab-destroyed? m))
なんかてきとーだけど、ないよりまし。はるかに。
予告
なんだかこの連載もわけがわからなくなってきたが、次回はいいかげんにすっ とばしてしまったあたりをもっぺん説明する予定。カンのいい人は、すでにGC の恐怖で震えが止まらなくなっているかもしれない。ないか。
ダウンロード
$ cvs -d:pserver:anonymous@cvs.sourceforge.jp:/cvsroot/gauche login Password: <リターン> $ cvs -d:pserver:anonymous@cvs.sourceforge.jp:/cvsroot/gauche co -r day4 Gauche-mecab
とすると Gauche-mecab というディレクトリができるので今日の内容を自分で試してみることができます。
前回までにGauche-mecabというディレクトリが用意されていれば、その中で
$ cvs up -r day4
とするだけでいいはずです。
質問・コメント
- Shiro: SCM_STRING_STARTはまずいです。そこで返って来る文字列はNULL終端 されていないかもしれません(どこかでNULL終端されていますが、その終端は ScmStringオブジェクトが意図している終端とは違うかもしれません)。 部分文字列が文字列本体を共有する場合があるためです。Scm_GetStringConst()を 使ってくださいな。
- Shiro: あと、<fixnum>と<int>の違いですが、<fixnum>は32bit machineで 30bitまでの数値しか扱わないかわりに、速いです。<int>はBignumかどうかの 検査も行うのでややオーバーヘッドがあるかわりに、32bitフルに使えます。
- "#" の前に空白があるエラーになる C コンパイラ(というか cpp)があったような気がする. 今時の環境なら気にしなくても良い?
- c89 以降なら大丈夫な筈ですよね? ansi2knr はこの辺直してくれないんだっけ?
- SunOS4 の標準 C コンパイラ(+ansi2knr) で Gauche は動くのだろうか。。。 HPUX8 以前はほぼ見込みなし。
第5の日
死のフーガ
日本の右翼ってなんで軍歌ばっかりかけるんだろうね。 伊福部昭でも流したほうがよっぽど戦意高揚するし人も釣れると思うんだけど。 このへんの手口はオウムに学べって誰に何を説明してんだ俺は。
autotools地獄(3)
automake 1.8は別に悪くなかった。1.8からAC_DEFUNの扱いがちょっと厳しく なったせいで(たぶん)*.m4の置き場所が1.5以前のとは別になっていただけだっ た。
Starting with Automake 1.8, `aclocal' will warn about all underquoted calls to `AC_DEFUN'. We realize this will annoy a lot of people, because `aclocal' was not so strict in the past and many third party macros are underquoted; and we have to apologize for this temporary inconvenience.
やっぱだめかも。
文字列にとって文字列とは何か
そっかー、やっぱりSCM_STRING_STARTはよっぽど自信がある場合しか使っちゃ いけなかったんだね。
m->m = mecab_new2((char*) Scm_GetStringConst(arg));
尻ぬぐい
前回いいかげんに流してしまった点が二つあった。まずは一つ目。 mecab_new2()の戻り値をチェックしていない。バインディングがどうとかとは 関係なく、Cのコードとしてこれは大変によろしくない。
マニュアル にはエラーのときに何がどうなるかの説明があまり明確に書かれていない。 拾い読みした感じでは、
- mecab_new2()は失敗したときにはNULLを返す
- その場合のエラーの内容はmecab_strerror(NULL)で取得できる
らしい。だからこんな処理を入れておかなきゃならない。
if (m->m == NULL) Scm_Error(mecab_strerror(NULL));
Scm_Error()は引数をエラーメッセージとして例外を投げる。てことは、ほうっ ておけばエラーを表示して終了する。
例外クラスの階層化は今のところGaucheのTODOになっている。つまり、例外を catchしてその種類によって処理を切り分けたりとかそういうことはできない。 まあエラーメッセージを解析すればできなくもないけど。 (WiLiKiの db-try-open では実際それに近いことをやっている)
Scm_Error()の親戚にScm_SysError()というのもある。こっちはstrerror()の 結果をメッセージに付け加えてくれるので、場合によっては便利。
それ以上のことが知りたければGHG:error.cへ。って空っぽだけど。
破壊/狂気
次の問題。Gaucheではいらなくなったオブジェクトは勝手にGCされて消えてし まう。それはいいんだけど、その際後始末をどうすればいいのか。 オブジェクトがファイルハンドルを握ってるなら離さないとならないし、 メモリを確保してるなら解放しなきゃならない。資源の独占はまずい。
オブジェクトがGCされるときに呼ばれるハンドラがあればよさそう なものだが、実はそういうのはちゃんとある。 finalizerという立派な名前までついている。
これで万事おっけーと筆者のような安直な人間なら考えるところだが、 これには問題がいろいろとある。らしい。どんなのかというと、 こんなの 。
In well-written programs there will typically be very few uses of finalization.
だって。どうしようかね。
gauche-devel でもややこしい議論が展開されたりしてたっけ。
まあいいや。筆者はめんどくさいのはパスな人間なので、やり方だけ説明して 逃げることにする。なんかまずいことが起きたらそのとき考えるということで。
finalizerはこんな感じで定義する。
static void mecab_finalize(ScmObj obj, void *data) { ScmMeCab *m = SCM_MECAB(obj); if (m->m != NULL) { mecab_destroy(m->m); m->m = NULL; } }
オブジェクトは渡してやるから、あとは好きなように始末をつけたまえという 態度。
これで思い出したけど、m->m != NULLのチェックは必要なのかどうなのか。 NULLを渡されたときにmecab_destroy()はどうふるまうのかが マニュアル には書いていない。 MeCabのソースをのぞいてみればこんな風になってるからまず大丈夫なんだけど、
void mecab_destroy (mecab_t *c) { if (c && c->allocated) { MeCab::Tagger *t = static_cast<MeCab::Tagger *>(c->ptr); delete t; delete c; } c = 0; }
経験からいうと、こういうのは安全を見てチェックを入れておいたほうが無難。 世間にはこのへん手を抜きまくってるライブラリも多い。 マニュアルに挙動が明記されていないなら安全側に振っておくべし。 てことでこうね。
(define-cproc mecab-destroy (m::<mecab>) " if (m->m != NULL) { mecab_destroy(m->m); m->m = NULL; } SCM_RETURN(SCM_UNDEFINED);")
でもって定義したfinalizerは登録してやらないと意味がない。
Scm_RegisterFinalizer(SCM_OBJ(m), mecab_finalize, NULL);
これでまあなんとか<mecab>オブジェクトがGCされても大丈夫そうな雰囲気に なってきた。
ダウンロード
$ cvs -d:pserver:anonymous@cvs.sourceforge.jp:/cvsroot/gauche login Password: <リターン> $ cvs -d:pserver:anonymous@cvs.sourceforge.jp:/cvsroot/gauche co -r day5 Gauche-mecab
とすると Gauche-mecab というディレクトリができるので今日の内容を自分で試してみることができます。
前回までにGauche-mecabというディレクトリが用意されていれば、その中で
$ cvs up -r day5
とするだけでいいはずです。
第6の日
ドキュメント
なんだかねー、最近思うんですけど、日本のプログラマーがドキュメントを書 きたがらないのって「日本人には契約の観念がない」とかいうのと密接な関係 があるんじゃないですかね。っつうか、そのまんまか。「ソースがドキュメン ト」というのはわかんなくもないけど、半分ネタだと思っておいたほうがいい と思う。
と、ここまで単純な話が理解されないとアレなので、くどくど説明する。ドキュ メントっていうのはある意味契約書なんですよ。製作者と利用者のあいだの。 もしドキュメントに書いてあるとおりに動かないならそれはバグなんで(ソフ トかドキュメントかの)、作者が悪い。ユーザーの側からすると、ドキュメン トされていない使い方をしてコケたとしても、それは自分を責めるしかない。 責任の所在を明確にできるんですね。
(だいたい「ソースがすべて」を突き詰めるならバグというのはありえないん じゃないだろうか。すべては仕様。書かれているままがすべて)
どうも日本人はこのへんを曖昧なままにしとくのが好きというか、良く言えば そういう世界が得意で(集団として見れば)能力を生かせるのかなーという気が する。だんだん変わっていくと思うけどね。
そういえば、GaUnitのkou氏が
ソースに書かれたドキュメントからマニュアルを生成するんじゃなくて,テスト(とそこに書かれたドキュメント)からマニュアルを生成できれば嬉しいんだけどなぁ.テストこそドキュメントであるという考えかた.
という興味深いアイディアを出している。うーん、どうせなら
コード + ドキュメント + テスト
をひとかたまりにしちゃったらどうだろう。ゴチャゴチャしすぎるかな。でも 本来は一体になっているべきものじゃない?
エラー
持病の記憶喪失が悪化したせいで本格的にワケがわかんなくなっている。なにやってたっ け。枠組みはできたのか。あとはわしわしバインドしていくだけか。そか。
じゃ、やる。まずはmecab_strerror()から。これはgdbm.stubにそっくりなの があるんで、マネしてやればいい。
(define-cproc mecab-strerror (m::<mecab>) " SCM_RETURN(SCM_MAKE_STR_IMMUTABLE(mecab_strerror(m->m)));")
…とうまくいくかどうか実はよくわからない。もしかしたらmecab-strerrorは malloc()した文字列を返すのかもしれない。だとしたらコピーを作ってから free()しなきゃならない。たぶんそれはない、と思うけど、他人の設計したも のに予断は禁物。
const char *mecab_strerror (mecab_t* m)
みたいにconstつきで宣言されていればそのへん安心できるんだけど、なし。 (実はC標準の strerror() にもなかったりする)
ドキュメントもされてない。ということは、コードを追いかけて何が戻ってき てるのか確認しなきゃならない、んだけど、めんどい。
いいや、省略。サンプルを見てもfree()してる様子はないんで、たぶん大丈夫 でしょ。だいたいMeCab自体はC++で書かれてて、APIだけがCなんで、メモリ管 理がどうなってるのかよくわからん。
説明が後先になったけど、SCM_MAKE_STR_IMMUTABLEはchar* をGaucheの immutable(変更不能) 文字列に変換するためのマクロ。親戚に SCM_MAKE_STR_COPYINGがある。どっちもScm_MakeString()を呼んでるんで、細か い挙動が知りたければstring.cを見てくださいな、と。
…いやダメだよやっぱり。戻ってきたやつの中身が変更される可能性があるな ら、COPYINGで実体コピーしなきゃならないもん。
よくわかんないときはこっちのが安全かなあ。効率は悪いけど。
(define-cproc mecab-strerror (m::<mecab>) " SCM_RETURN(SCM_MAKE_STR_COPYING(mecab_strerror(m->m)));")
最近の富豪的プログラマーなら「そんなのいちりつコピーでいいじゃん」と 思うかもしらんが、Gaucheはこういうところはけっこうマメに稼ぐ主義らしい。
(そういえば、ソフトウェアの世界では省エネってさっぱり言わんよね。その うち叩かれると思うけど)
パース
次、mecab_sparse_tostr()。ああ、ようやく解析できるのね。めかぶなのね。
char *mecab_sparse_tostr (mecab_t *m, char *str)
どうしよう。str引数にconstがついてないよ。それはつまり mecab_sparse_tostr()内部で中身を変更するかもしれないっていうこと。それは Gauche的には非常にまずいの。Gaucheの文字列は read-only なの。プロトタイプだけ見て書くならいちいち文字列のコピーを作って 渡さなきゃならないの。本当にそうなの。ドキュメントにも説明がないの。 助けてお母さんなどと退行していてもしょうがないので現実的に対処する。
とりあえずconstがついているつもりでいく。まずかったら後で直そう。 だいたい前に一回やっちゃってるんだよね。
m->m = mecab_new2((char*) Scm_GetStringConst(arg));
のとこ。キャストでconstを潰してる。これ見て不安に思わなかった人は、今 からでも反省室に直行されなされ。というか、自分で行けばいいよね。(以下 反省室で執筆)
上は
m->m = mecab_new2(Scm_GetString(arg));
のほうが安全です。はい。
mecab_sparse_tostr()の戻り値についてはちゃんとドキュメントされている。
- 失敗したらNULL
- 成功した場合のポインタの指す領域はこっちでは管理する必要なし
- ただし、後続の呼び出しで上書きされるので注意
これだけ情報が揃ってれば安心。「失敗」する条件が気になるけど。
えーと、どういうインターフェースにすればいいのかな。きっとgdbm-fetchの マネすればいいよね。成功したときは文字列を返し、失敗したときは#f。で、 どんなエラーだったのか知りたければ自分でmecab-strerrorを呼んでね、と。
(define-cproc mecab-sparse-tostr (m::<mecab> str::<string>) " char *r = mecab_sparse_tostr(m->m, (char*) Scm_GetStringConst(str)); if (r != NULL) SCM_RETURN(SCM_MAKE_STR_COPYING(r)); else SCM_RETURN(SCM_FALSE);")
SCM_FALSEっていうのは#fな定数。当然のようにSCM_TRUEもあります。
テスト
いや前から気になってたんすけどね。日本語の入るテストケースってどう書い たらいいんでしょうね。マジメにやるならデータをtest.scmの外に出すんです か。charconvみたいに。ちょこちょこっと書きたいときはめんどうだなあ。
それ以前にMeCabが取る文字列のエンコーディングってなんだろう。APIのほう には説明がないけど、 コマンドライン版の説明 を見るかぎりEUCか。うーん、どうしよ。
やっぱり最終的には外に出すべきか。でも今日のところはtest.scmに直書きで 勘弁。まだどういうテストがいるのかわかんないしね。
mecab-strerrのテストはこんなふうにしてみた。
(test* "mecab-sparse-tostr" #f (mecab-sparse-tostr m "太郎は次郎が持っている本を花子に渡した。")) (test* "mecab-strerror" #t (string? (mecab-strerror m)))
これはmをdestroyしたあとに実行されているので、"mecab_sparse_tostr: first argment seems to be invalid"というエラーが戻ってくる。
もっぺんmecabオブジェクトを生成してからだと、うまくいくはず。
(define m (mecab-new2 "")) (test* "mecab-sparse-tostr" "太郎?t名詞,固有名詞,人名,名,*,*,太郎,タロウ,タロー?nは?t助詞,係助詞,*,*,*,*,は,ハ,ワ?n次郎?t名詞,固有名詞,人名,名,*,*,次郎,ジロウ,ジロー?nが?t助詞,格助詞,一般,*,*,*,が,ガ,ガ?n持っ?t動詞,自立,*,*,五段・タ行,連用タ接続,持つ,モッ,モッ?nて?t助詞,接続助詞,*,*,*,*,て,テ,テ?nいる?t動詞,非自立,*,*,一段,基本形,いる,イル,イル?n本?t名詞,一般,*,*,*,*,本,ホン,ホン?nを?t助詞,格助詞,一般,*,*,*,を,ヲ,ヲ?n花?t名詞,一般,*,*,*,*,花,ハナ,ハナ?n子?t名詞,一般,*,*,*,*,子,コ,コ?nに?t助詞,格助詞,一般,*,*,*,に,ニ,ニ?n渡し?t動詞,自立,*,*,五段・サ行,連用形,渡す,ワタシ,ワタシ?nた?t助動詞,*,*,*,特殊・タ,基本形,た,タ,タ?n。?t記号,句点,*,*,*,*,。,。,。?nEOS?n" (mecab-sparse-tostr m "太郎は次郎が持っている本を花子に渡した。"))
leque氏の言ってたラッパーって、この結果をSchemeで処理しやすいよう にパースしてくれるものなのかなあ。でも出力フォーマットはXMLやCSVにもで きるらしいし、そのへんどうなんでしょ。
ほい
それ以前にGaucheの内部エンコーディングがMeCabと一致してないと文字列を 渡せないじゃないか。コード変換しなきゃならないじゃないか。
ダウンロード
$ cvs -d:pserver:anonymous@cvs.sourceforge.jp:/cvsroot/gauche login Password: <リターン> $ cvs -d:pserver:anonymous@cvs.sourceforge.jp:/cvsroot/gauche co -r day6 Gauche-mecab
とすると Gauche-mecab というディレクトリができるので今日の内容を自分で試してみることができます。
前回までにGauche-mecabというディレクトリが用意されていれば、その中で
$ cvs up -r day6
とするだけでいいはずです。
質問・コメント
- 出力をパースするよりも、出力フォーマットを自分で定義してしまう方が楽だと思います。
うちでは、 まず、mecab_new2 の引き数があつかいにくかったので、make-mecab みたいな関数を作って、(make-mecab :rcfile "mymecabrc") ; (mecab-new2 "--rcfile=mymecabrc") と同等
みたいなかたちで mecab-new2 の引き数を指定できるようにしておいて、 --bos-format オプションや --node-format オプションで出力形式を適宜指定していました。
例えば、分かち書きをして、その結果を文字列のリストとして受け取りたい場合は(make-mecab :bos-format "(" :node-format "?"%m?" " :eos-format ")")
として、結果を read-from-string していました。で、さらに形態素を適当なクラスのインスタンスとして 扱いたい場合は、make メソッドのキーワード引き数のリストを同様にして作って apply するという感じです。 -- leque