29 July 2009

方針転換

しばらく日記っぽくブログを書いてみます。

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->stop_word($_) );
  }
  foreach (qw(one two three four five)) {
    ok( !$stopper->stop_word($_) );
  }
  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 add_valuerangeprocessor {
  my ($self, $vrproc) = @_;
  push @{$self{_vrproc}}, $vrproc;   # 変更点
  add_valuerangeprocessor1( @_ );
}

なんとなく勘で、@{}で変数を囲ってみたら、うまいこと配列として扱ってくれたみたいです。なんでこれで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 );

以下は、上の最後の行のadd_valuerangeprosesccorの中で、メンバ変数をプリントした様子。うまくいっているようです。

2966:       my ($self, $vrproc) = @_;
  DB<2> l
2966==>     my ($self, $vrproc) = @_;
2967:       push @{$self{_vrproc}}, $vrproc;
2968:       add_valuerangeprocessor1( @_ );
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"というアクセサを要求しているみたいです。このように、アクセサの使用を強要することってできるんでしょうか。それとも使い方を間違ってるだけ?あとで要調査です。