PersistentPerlでCGI.pm(ry 原因と対処策

PersistentPerlを使うと2回目以降のリクエストでCGI.pmが-no_xhtmlを忘れてしまう件の、原因と対処策。

  1. PersistentPerl 2.22
  2. CGI.pm 3.15

CGI.pmにデバッグプリントを埋め込みまくって前回の再現ケースを実行すると、こうなる。

$ touch test.pl; ./test.pl 1; ./test.pl 2
##### >initialize_globals XHTML=
##### <initialize_globals XHTML=1
##### >import XHTML=1
##### <import XHTML=0
##### 1
##### >new XHTML=0
##### <new XHTML=0
<!DOCTYPE html
        PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
         "http://www.w3.org/TR/html4/loose.dtd">
<html lang="en-US"><head><title>Untitled Document</title>
</head>
<body>
##### 2
##### >new XHTML=1
##### <new XHTML=1
<!DOCTYPE html
        PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
         "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en-US" xml:lang="en-US">
<head>
<title>Untitled Document</title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
</head>
<body>
  1. -no_xhtmlの設定値は$XHTMLというパッケージ変数に格納されている。1ならXHTML、0ならHTML。
  2. use CGIすると最初に内部でinitialize_globals()が呼ばれ、$XHTMLが1に設定される。ここは固定値。
  3. 次にimport()が呼ばれユーザ設定が読み込まれる。-no_xhtmlを指定していればここで$XHTMLは0になる。
  4. 初回の試行は意図どおり。
  5. 2回目、CGI->new()をした時点で$XHTMLが1になってしまっている。

initialize_globals()とimport()はパッケージをロードしたときに一度しか呼ばれないものなので、2回目の試行時には実行されない。にもかかわらずこのように$XHTMLの値が書き換わってしまう。
1回目と2回目の試行の間に$XHTMLを勝手に書き換えているのは誰か。実は…

$ cat src/perperl_perl.c
〜
static int onerun(int single_script) {
    〜
    /* Hack for CGI.pm */
    my_call_sv(get_perlvar(&PERLVAR_RESET_GLOBALS));

PersistentPerl本体!そんなのわかるか。
ここのmy_call_sv()によって呼ばれる処理は、CGI.pmの以下の部分。

sub _reset_globals { initialize_globals(); }

ということで原因をまとめると、

  • CGI.pmはパッケージ変数を書き換えながら動く行儀の悪いモジュール。
  • PersistentPerlは実行の都度_reset_globals()を呼んでパッケージ変数を初期化することで、これに無理やり対処した。ちなみにmod_perlの場合は、CGI.pm側に_reset_globals()を呼ぶルーチンが入っている。ずるい。
  • ところがCGI.pm 3.15では、_reset_globals()を実行しただけでは初期化になっていない。あわせてimport()も実行する必要がある。
  • そのため、2回目以降は開発者の意図と異なる設定で動いてしまっている。

となる。
対処方法は何でもいいんだけど、基本的にはアプリケーション側で対処するのがよいかと。PersistentPerl本体やCGI.pmにパッチをあてると維持管理がめんどくさいので。

#!/usr/bin/perperl -w
print "##### $ARGV[0]\n";
use CGI;
CGI->import(-no_xhtml);
my $cgi = CGI->new();
print $cgi->start_html();

要するに毎回正しく初期化しなさいと、それだけ。

$ touch test.pl; ./test.pl 1; ./test.pl 2
##### >initialize_globals XHTML=
##### <initialize_globals XHTML=1
##### >import XHTML=1
##### <import XHTML=1
##### 1
##### >import XHTML=1
##### <import XHTML=0
##### >new XHTML=0
##### <new XHTML=0
<!DOCTYPE html
        PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
         "http://www.w3.org/TR/html4/loose.dtd">
<html lang="en-US"><head><title>Untitled Document</title>
</head>
<body>
##### 2
##### >import XHTML=1
##### <import XHTML=0
##### >new XHTML=0
##### <new XHTML=0
<!DOCTYPE html
        PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
         "http://www.w3.org/TR/html4/loose.dtd">
<html lang="en-US"><head><title>Untitled Document</title>
</head>
<body>

やっと直った。