Perlでプラグインという仕組みを利用するには <1>

//長くなりそうだから、タイトルに章番号付けてみた。

自分でフレームワーク・ライブラリ・モジュールといったものを作ったりすると、それ自体に拡張性を持たせるために『プラグイン』という仕組みを導入したくなる時があると思います。例えば…

  • Apacheのモジュールのように各フェーズ(リクエストヘッダの解析、アクセス制御、コンテンツの出力、等々)毎にあとからプラグインによって機能を追加していく(フック=hookしていく)もの
  • CGI::Applicationのようにやはり各フェーズにフックする、または、任意の関数をフレームワークに追加するもの

ここでは特にCGI::Applicationを参考に見ていこうと思います。上でもちょっと触れていますが、CGI::Application(以下CAP)にはおおまかに2通りのプラグインがあります。それぞれ説明します(用語は便宜上私が勝手に付けました)。

フック形式
各処理の前後(init, prerun, postrun, teardown, etc)に任意の処理を差し込むもの
関数追加形式
CGI::Application::Plugin::Sessionのように後からCGI::Application自体に関数を追加するもの*1。ちなみにCGI::Application::Plugin::Sessionはsession, session_configといった関数を追加しますね。

さぁ、それぞれどうやって実現されているのか。CAPを実例に説明していく。ぜひ、CAPのソースコード片手に見ていただきたい。

フック形式

フック形式はCAPのフックを呼び出す&call_hookから見ていく。

sub call_hook {
    my $self      = shift;
    my $app_class = ref $self || $self;
    my $hook      = lc shift;
    my @args      = @_;

    die "Unknown hook ($hook)" unless exists $INSTALLED_CALLBACKS{$hook};

    my %executed_callback;

    # First, run callbacks installed in the object                                                  
    foreach my $callback (@{ $self->{__INSTALLED_CALLBACKS}{$hook} }) {
        next if $executed_callback{$callback};
        eval { $self->$callback(@args); };
        $executed_callback{$callback} = 1;
        die "Error executing object callback in $hook stage: $@" if $@;
    }

    # Next, run callbacks installed in class hierarchy                                              

    #.... 省略

}

一部省略したが、大事なのはforeachの中だ。$hookにはinitやprerunといったフック名が入る。そして$callbackには関数名(または関数へのリファレンス)が入る。そうするとeval { $self->$callback(@args)};の意味が分かると思う。__INSTALLED_CALLBACKSで取り出したコールバック関数名の配列を順次実行しているわけだ。

さて、なんとなく理解出来たと思うのだけど、一つ疑問に思わなくてはいけない事がある。それは$self->$callback()でコールバックを呼んでる言っても、あとからプラグインで追加した関数がなぜ、$selfの中にあるのか? どちらかと言えば、$plugin->$callback()のほうが自然に見えないだろうか?

ここで、$self->$callbackというように呼び出せる理由を説明しなくてはいけないのだけど、疲れてきたので、明日。

ヒントは次の関数とExporter::importのソースコード、かな。

sub import {
    my $caller = scalar(caller);
    $caller->add_callback('init', 'my_setup');
    goto &Exporter::import;
}

*1:実際はCAPに関数を追加しているわけではないのだけど、そう見えるので便宜上こう説明しておく