方針転換
しばらく日記っぽくブログを書いてみます。
QueryParser::set_stopper() と SimpleStopperオブジェクトのスコープ
GSoCでは、Xapianという検索エンジンのperlバインディングをSWIGで作るというプロジェクトを行っています。
今日はQueryParserというクラスのparse_queryというメソッドのバグを修正していました。
現象
以下のテストコードが通りません。というか、途中でbus errorでプログラムが終了します。
my $qp = new Search::Xapian::QueryParser();# …
{ my @stopwords = qw(a the in on and); my $stopper; ok( $stopper = new Search::Xapian::SimpleStopper(@stopwords) ); foreach (@stopwords) { ok( $stopper->stopword($) ); } foreach (qw(one two three four five)) { ok( !$stopper->stopword($) ); } ok( $qp->set_stopper($stopper), undef ); }
ok( $qp->parse_query(“one two many”) ); # ここで落ちる
原因
原因はSimpleStopperのスコープでした。上のコードの真ん中あたりで、set_stopperというメソッドでSimpleStopperオブジェクトをQueryParserへセットしてます。ここで、{}のブロックを抜けると、セットしたSimpleStopperのスコープが外れてしまうのが問題です。
解決法
pythonのバインディングでは、set_stopper()を呼び出すと、QueryParserオブジェクトの中にSimpleStopperへのリファレンスを保持することで、この問題を回避していました。おそらく、SimpleStopperを指すリファレンスを一つ増やすことで、ガベージコレクタに回収されてしまうのを防いでいるのだと思います。(未検証。後で調べる)
よってperlの場合でも同様のアプローチをとります。まず、従来からあるset_stopper()関数をset_stopper1()へリネームします。次に新しくset_stopper()関数を作ります。個の中で、SimpleStopperのリファレンスの格納と、set_stopper1()の呼び出しを行います。
またpythonバインディングのコメントより、どうもXapian本体のバグでもあるようです。たぶんこのチケットが詳細。後で読む。
#186 (User subclassable classes should be reference counted) – Xapian
検証
SWIGのインターフェースファイルに以下を追記しました。
%rename(set_stopper1) Xapian::TermGenerator::set_stopper(const Xapian::Stopper * stopper); # ... %perlcode %{ # ... sub set_stopper { my ($self, $stopper) = @_; $self{_stopper} = $stopper; set_stopper1( @_ ); } # ... %}
無事テストの問題の箇所は通過しました。
todo
- perlでのガベージコレクタの動作
- チケット#186を読む
配列のハッシュ
上記と同様の原因のエラーが別のクラスやメソッドにも発生したので、同じアプローチで修正していきます。ただし、QueryParser::add_valuerangeprocessors()が例外です。こちらはValueRangeProcessorオブジェクトを何個も追加していけるという関数です。上記のように、_stopperメンバにリファレンスを保持していくやり方では、最後の追加したオブジェクト一個だけしか保持できず、今回の用途には不適切です。よって、_vrprocという配列にValueRangeProcessorのリファレンスをどんどんプッシュしていく方法にしました。
コードはこんな感じになります。
sub addvaluerangeprocessor { my ($self, $vrproc) = @; push @{$self{_vrproc}}, $vrproc; # 変更点 addvaluerangeprocessor1( @ ); }
なんとなく勘で、@{}で変数を囲ってみたら、うまいこと配列として扱ってくれたみたいです。なんでこれでokなのか理解しきってないので、あとで要調査。
次のコードをデバッガで調べてみると、うまく動いているようです。
$qp = new Search::Xapian::QueryParser(); my $vrp1 = new Search::Xapian::DateValueRangeProcessor(1); my $vrp2 = new Search::Xapian::NumberValueRangeProcessor(2); my $vrp3 = new Search::Xapian::StringValueRangeProcessor(3); my $vrp4 = new Search::Xapian::NumberValueRangeProcessor(4, ’$’); my $vrp5 = new Search::Xapian::NumberValueRangeProcessor(5, ‘kg’, 0); $qp->add_valuerangeprocessor( $vrp1 ); $qp->add_valuerangeprocessor( $vrp2 ); $qp->add_valuerangeprocessor( $vrp4 ); $qp->add_valuerangeprocessor( $vrp5 ); $qp->add_valuerangeprocessor( $vrp3 );
以下は、上の最後の行のaddvaluerangeprosesccorの中で、メンバ変数をプリントした様子。うまくいっているようです。
2966: my ($self, $vrproc) = @; DB<2> l 2966==> my ($self, $vrproc) = @_; 2967: push @{$self{_vrproc}}, $vrproc; 2968: addvaluerangeprocessor1( @ ); 2969 } 2970
2971 package Search::Xapian::SimpleStopper; 2972 sub new { 2973: my $class = shift; 2974: my $stopper = Search::Xapianc::new_SimpleStopper(); 2975
DB<2> p $self{_vrproc} ARRAY(0x8a7b9c) DB<3> p $self{_vrproc}[0] Search::Xapian::StringValueRangeProcessor=HASH(0x8a7cd4) DB<4> p $self{_vrproc}[1] Search::Xapian::DateValueRangeProcessor=HASH(0x8a7e00)
ここで一つ疑問点。クラスのメンバ変数へ、外部から直接アクセスすることができないように見えたんですが、原因がよく分かりません。例えば、
$qp = new Search::Xapian::QueryParser(); my $vrp1 = new Search::Xapian::DateValueRangeProcessor(1); $qp->add_valuerangeprocessor( $vrp1 );print $qp->{_vrproc}[0], “\n”;
このようにしても、プリントできませんでした。エラーメッセージを見てみると、どうも”swig_変数名_get”というアクセサを要求しているみたいです。このように、アクセサの使用を強要することってできるんでしょうか。それとも使い方を間違ってるだけ?あとで要調査です。