コールバック関数の登録とweakenとオブジェクトの寿命について


だいぶ習熟したつもりでも、いまだに`weaken`にはビビらされるのでメモ。

# 検証環境

- Xubuntu 12.04 32bit
- perl v5.14.2 (Ubuntuレポジトリに入ってるやつ) 
- AnyEvent 7.05
- EV 4.15

# 概要

オブジェクトにコールバック関数を登録する場合、循環参照を防ぐためにコールバック関数にクロージャとして取り込む変数を`weaken`するのはよくあることだが、その場合、 **コールバック関数の実行内容によって** weak変数の指すオブジェクトがDESTROYされる可能性があることに注意しないといけない。

# 例

このあいだ、こんな感じのモジュールを書いた。

```perl:Wrapper.pm
package Handle::Wrapper;
use strict;
use warnings;
use AnyEvent::Handle;
use Scalar::Util qw(weaken);

sub new {
    my ($class, $fh) = @_;
    my $self = bless {
        handle => AnyEvent::Handle->new(fh => $fh),
        next_read_callbacks => []
    };
    $self->_init_callbacks();
    return $self;
}

sub _init_callbacks {
    my ($self) = @_;
    weaken $self;  ######################################### (1)
    $self->{handle}->on_read(sub {
        my ($handle) = @_;
        my $data = $handle->{rbuf};
        ## my $strong_self = $self;  ####################### (2)
        $handle->{rbuf} = "";
        for my $cb (@{$self->{next_read_callbacks}}) {  #### (3)
            $cb->($data) 
        }
        $self->_clear_next_read();  ######################## (4)
    });
}
    
sub _clear_next_read {
    my ($self) = @_;
    @{$self->{next_read_callbacks}} = ();
}

sub register_next_read {
    my ($self, $callback) = @_;
    push(@{$self->{next_read_callbacks}}, $callback);
}

sub DESTROY {
    warn "Handle destroyed.\n";
}

1;
```

要は`AnyEvent::Handle`のラッパーであり、`next_read_callbacks`に格納したコールバック関数を、ファイルハンドルから読み出したデータに対して1回だけ起動する、というもの。

この機能を実現するために、`_init_callbacks`メソッドで`AnyEvent::Handle`の`on_read`メソッドを読んでコールバック関数を登録しているが、その前に`weaken $self;`を実行している(1)。これは`on_read`コールバックの中で`$self`を使っているため(3,4)である。`weaken`をしないとコールバック関数というクロージャを介した循環参照が発生し、`$self`がメモリから解放されなくなる。

このように循環参照によるメモリリークを防ぐために`weaken`は重宝するわけだが、逆に循環参照を駆使してオブジェクトの寿命を延ばすテクニックもある。

例えば、上記のモジュールは以下のようにして使う。

```perl:wrapper_user.pl
package main;
use strict;
use warnings;
use EV;
use AnyEvent;
use AnyEvent::Socket qw(tcp_server);
use AnyEvent::Handle;
use Handle::Wrapper;

my $cv = AnyEvent->condvar;

tcp_server "127.0.0.1", 18888, sub {
    my $fh = shift;
    my $handle = Handle::Wrapper->new($fh);
    $handle->register_next_read(sub {  ########## (5)
        my $data = shift;
        warn "Received $data\n";
        undef $handle;  ######################### (6)
        warn "undef handle\n";
        $cv->send;
    });
};

my $client_handle = AnyEvent::Handle->new(
    connect => ["127.0.0.1", 18888],
    on_connect => sub {
        my ($handle) = @_;
        $handle->push_write("foobar");
    },
);

$cv->recv;
```

TCPコネクションを受け付けると`Handle::Wrapper`オブジェクトを作成し、コールバック関数を登録する(5)。この時、コールバック関数内に`undef $handle;`と書くことで、意図的に循環参照を作る(6)。`$handle`変数のスコープは`tcp_server`のコールバック関数内限定だが、循環参照があるため、`$handle`オブジェクトはメッセージ受信まで存続する。`AnyEvent`ではこういった意図的な循環参照がしばしば用いられる。

さて、上記スクリプトを実行すると以下のような結果になる。

```
$ perl wrapper_user.pl 
Received foobar
Handle destroyed.
undef handle
EV: error in callback (ignoring): Can't call method "_clear_next_read" on an undefined value at wrapper_user.pl line 28.
```

これは、`undef $handle;`の実行時点(6)で`$handle`オブジェクトがDESTROYされてしまい、その結果、(4)の実行時点で`$self`が`undef`になってしまったからである。このように、コールバック関数の実行によって`weaken`した変数の指すオブジェクトがDESTROYされることは起こりうる。

余談ではあるが、`_clear_next_read`メソッドの中身をそのまま(4)に展開すると例外は発生しない。これは`undef`となった`$self`をHASHREFとして参照することでautovivificationが発生し、`$self = {}`と`$self->{next_read_callbacks} = []`が暗黙的に実行されているからであり、これはこれで厄介なバグを生む可能性がある。

これを防ぐためには(2)をコメントアウトを解除すればいい。

```perl
my $strong_self = $self;
```

すると実行結果は以下のようになる。

```
$ perl wrapper_user.pl 
Received foobar
undef handle
Handle destroyed.
```

`$strong_self`が`on_read`コールバック関数のスコープ内に存在することで、`next_read_callbacks`内の関数が何をしようとも実行中は`$handle`オブジェクトへの参照可能性が担保される。そのため、`$self`は`undef`とならない。上記の例の場合、`on_read`コールバック関数が終了した後に`$strong_self`は消滅するので、この時点で`$handle`は無事解放される。