XPathFeedのテスト実行時の"Use of uninitialized value $url ..."を消す

Ensure weak references in use in HTML::TreeBuilder? by vzvu3k6k · Pull Request #5 · onishi/xpathfeedについての覚え書き。

t/XPathFeed.tを実行すると Use of uninitialized value $url in pattern match (m//) at lib/XPathFeed.pm line 91. Use of uninitialized value $url in concatenation (.) or string at lib/XPathFeed.pm line 91. という警告が表示される。これがエラーとまぎらわしいので消したかった。

sub uri {
    my $self = shift;
    my $url = $self->url;
    $url =~ /http/ or $url = "http://$url"; # line 91
    $self->{uri} ||= URI->new($url)->canonical;
}

問題のコードはXPathFeed::uriにあるのだが、XPathFeedのインスタンスを作ってそのまま捨てる(carton exec -- perl -Ilib -MXPathFeed -e'XPathFeed->new')だけでもこの警告が出る。

しかし、インスタンスを初期化するときにuriメソッドが呼ばれているわけではなさそう。

sub new {
    my $class = shift;
    my %args = @_==1 ? %{$_[0]} : @_;
    return $class->SUPER::new({%args});
}

親クラスのuse base qw/Class::Accessor::Fast Class::Data::Inheritable/;あたりが呼び出しているのかな?と思いながら、callerでuriメソッドの呼び出し元を追ってみると、実はデストラクタが原因だった。

sub clean {
    my $self = shift;
    $self->tree or return;
    $self->tree->delete;
}

sub DESTROY { # これがデストラクタ
    my $self = shift;
    $self->clean;
}

XPathFeedのインスタンスがGCされるとき、$self->DESTROYが呼び出され、その中で$self->cleanが実行される。ここではメモリリークを避けるため、$self->tree->deleteでHTML::TreeBuilderのインスタンスが持っているHTML要素などを明示的に破棄している。ところが、$self->treeは初回実行時に$self->decoded_contentからHTML::TreeBuilderのインスタンスを遅延初期化するメソッドなので、ここで意図せずtreeが生成されてしまう。treeを生成するために$self->decoded_contentが呼ばれ、そこから$self->http_resultが呼ばれ、その中で$self->uriが呼ばれているのだった。さらにキャッシュやLWP::UserAgentなども呼び出され、ようやく生成されたtreeは次の行でバサリと切り倒される。

解決方法

一番簡単な解決方法は、uriメソッドをmy $url = $self->url || '';のように書き換えることだろう。しかし、空のインスタンスを破棄するだけで大量のメソッドが無意味に実行される問題は残る。

XPathFeed->newに常にダミーのurlパラメータを渡すのも同様。

別の方法として、$self->treeで生成されたHTML::TreeBuilderのインスタンスが$self->{tree}に保存されるのを利用する手もある。cleanメソッドで直接$self->{tree}を参照してやれば、treeを削除するために生成するような無駄はなくなる。とはいえ、内部で使われることを意図した変数に直接アクセスするのは好ましくない気もする。

弱い参照

なぜHTML::TreeBuilder::deleteを呼ばないとメモリリークが発生するのかというと、ノードツリーが親子で循環参照しているので、参照カウント方式のGCだとオブジェクトが不要になったことが検出できないからだそうだ。

最近のPerlには弱い参照が実装されていて、Scalar::Util::weakenで指定した参照はカウントされなくなる。実はHTML::TreeBuilderが内部で使っているHTML::Elementでは、Scalar::Util::weakenが存在していれば自動的に弱い参照を使う。これなら$self->tree->deleteを呼ぶ必要はなくなる。

use HTML::TreeBuilder 5 -weak;とオプションを与えてuseすると弱い参照を使うように強制できる(Scalar::Util::weakenが存在しなければエラーが出る)。これならXPathFeed::cleanやDESTROYを削除できる。


sub clean {
    my $self = shift;
    return if HTML::TreeBuilder->Use_Weak_Refs || !$self->tree;
    $self->tree->delete;
}

このように弱い参照が有効になっているか確認するコードを入れるだけで済ませることも考えたが、できればコードの量を減らしたい。どうやら弱い参照は5.8あたりから使えるようになったらしいので、この機能が使えることを前提にしてもよさそう。

弱い参照はいつごろからサポートされているのか

perl56deltaweak referenceが導入されたというアナウンスがある。当時はexperimentalな機能で、Devel::WeakRefを通じて利用することができた。

perl58deltaでScalar::Utilが追加されていて、この中にweakenも入っている(Scalar::UtilをPerlにバンドルするコミット)。weakenなどの内部ではSvWEAKREFが定義されているかチェックしているが、5.8ではsv.hで無条件に定義されている様子で、それ以降のperldeltaにはこの機能を無効化したという記述はないので、5.8以降では常に使える状態なのではないかと思う。

参考

useのオプションの伝播

HTML::TreeBuilderで弱い参照を強制するにはuse HTML::TreeBuilder 5 -weak;と指定せよとドキュメントには書いてある。ところが、HTML/TreeBuilder.pmにはuseのオプションを直接チェックする処理はない。-weakを実際に解釈しているのはHTML::Elementというモジュールだ。

HTML/TreeBuilder.pmの中ではuse HTML::Element ();とuseに明示的に空リストが渡されていて、-weakを引き渡している様子はない。それではどうやって値がHTML::Elementに渡されているのかというと、継承によって実現されている。

-weakのようなオプションは__PACKAGE__->import()に渡される。HTML::TreeBuilder::importが存在しないので、our @ISA = qw(HTML::Element HTML::Parser);で親クラスに設定されているHTML::Elementのimportが呼び出されるという仕組み。

ちなみに最初の5というオプションはHTML::TreeBuilderのバージョンが5以上であることを保証させるオプション。これは__PACKAGE__->VERSION()に引数として渡されている。デフォルトではベースクラスのUNIVERSAL::VERSIONが呼び出され、指定されたバージョンよりクラスの$VERSIONが古ければdieされる。

参考

その他

XPathFeedにプルリク送りまくってたらcollaboratorにしてもらえた。特にGitHubから通知とかはなくて、ある日突然https://github.com/のYour repositoriesにonishi/xpathfeedという表示が追加されたという感じだった。