ファイルハンドルをめぐる冒険(ただしマニア向け)
以下は Perl 5.8.8 のソースを元に記述しました。Perl 5.10 でもそう変わってはいないと思いますが,結構内部が変更されているので違うかもしれません。大まかには同じだと思います。
イントロダクション
Perl でのファイルハンドルは内部的には [http://search.cpan.org/perldoc?IO::Handle:title=IO::Handle]
オブジェクトとして保持されています。そのような記述は perl5004delta くらいでしか見つかりませんでした。ラクダ本にもなかったような。
perl5004deltaInternal change: FileHandle class based on IO::* classes
File handles are now stored internally as type IO::Handle. The FileHandle module is still supported for backwards compatibility, but it is now merely a front end to the IO::* modules -- specifically,
IO::Handle, IO::Seekable, and IO::File. We suggest, but do not require, that you use the IO::* modules in new code.In harmony with this change, *GLOB{FILEHANDLE} is now just a backward-compatible synonym for *GLOB{IO}.
ですから,実は [http://search.cpan.org/perldoc?IO::Handle:title=IO::Handle]
を use
しておくと,ハンドルに対して [http://search.cpan.org/perldoc?IO::Handle:title=IO::Handle]
のインスタンスメソッドを使うことができます。
use strict; use warnings; use IO::Handle; open my $handle, '>', 'output.txt' or die "open: $!"; $handle->print("Hello, world!\n"); $handle->close();
個人的にそこそこ使ってきたフィーチャーなんですけれど,あまりおすすめしません(その理由は後述します)。
use IO::Handle
してないと,[http://search.cpan.org/perldoc?IO::Handle:title=IO::Handle]
にどのようなメソッドが生えているかわからないので,いつものように怒られます。
use strict; use warnings; #use IO::Handle; open my $handle, '>', 'output.txt' or die "open: $!"; $handle->close(); # ERROR: Can't locate object method "close" via package "IO::Handle" close $handle; # you can close $handle as usual
Can't locate object method "close" via package "IO::Handle"
って怒られてますね。なのでファイルハンドルは [http://search.cpan.org/perldoc?IO::Handle:title=IO::Handle]
なオブジェクトとみなしてよさそうです。
ファイルハンドルは [http://search.cpan.org/perldoc?IO::Handle:title=IO::Handle]
オブジェクト と等価か?
じゃあ $handle
が [http://search.cpan.org/perldoc?IO::Handle:title=IO::Handle]
に bless
されているか調べてみましょう。
use strict; use warnings; use Scalar::Util qw( blessed ); use Data::Dumper; use IO::Handle; sub say { print join q{}, @_, "\n"; } open my $handle, '>', 'output.txt' or die "open: $!"; say $handle; # GLOB(0x8153c28) say ref $handle; # GLOB say defined blessed $handle ? blessed $handle : 'unblessed'; # unblessed say Dumper($handle); # $VAR1 = \*{'::$handle'}; $handle->close();
あくまでグロブのリファレンスであり bless
されているわけではないみたいです。
UNIVERSAL::can
等で調べてみます。
use strict; use warnings; use IO::Handle; sub say { print join q{}, @_, "\n"; } open my $handle, '>', 'output.txt' or die "open: $!"; say $handle->isa('IO::Handle') ? 'isa IO::Handle' : 'not IO::Handle'; # "not IO::Handle" ! say $handle->can('close') ? 'can close' : 'cannot close' ; # "cannot close" !! $handle->close(); # still you can close() !!!
なななんと,ファイルハンドルは [http://search.cpan.org/perldoc?IO::Handle:title=IO::Handle]
の派生物ではなく,close()
も呼び出せない,ということになっているようです。実際には呼び出せるのに。これが前述した「おすすめできない理由」です。
でも「Can't locate object method "close" via package "IO::Handle"
」っていわれたのはなんだったんでしょうか?
Devel::Peek で覗いてみる
Devel::Peek モジュールの Dump()
関数を使うと,Perl の変数等の内部構造を見ることができます。
use strict; use warnings; use Devel::Peek; open my $handle, '>', 'output.txt' or die "open: $!"; print Dump($handle); close $handle;
以下出力結果です。
SV = RV(0x81aab88) at 0x8154684 REFCNT = 1 FLAGS = (PADBUSY,PADMY,ROK) RV = 0x8153c28 SV = PVGV(0x8177470) at 0x8153c28 REFCNT = 1 FLAGS = (GMG,SMG) IV = 0 NV = 0 MAGIC = 0x817e108 MG_VIRTUAL = &PL_vtbl_glob MG_TYPE = PERL_MAGIC_glob(*) MG_OBJ = 0x8153c28 NAME = "$handle" NAMELEN = 7 GvSTASH = 0x8153b50 "main" GP = 0x81b0380 SV = 0x8153d48 REFCNT = 1 IO = 0x8154624 FORM = 0x0 AV = 0x0 HV = 0x0 CV = 0x0 CVGEN = 0x0 GPFLAGS = 0x0 LINE = 10 FILE = "test.pl" FLAGS = 0x0 EGV = 0x8153c28 "$handle"
perlguts に触れたことのない人はおえっと思うかもしれません。ともかく [http://search.cpan.org/perldoc?IO::Handle:title=IO::Handle]
の痕跡は見当たりません。
ですが,
$handle
はたしかにグロブ(GV
)へのリファレンス(RV
)であること- グロブ内の
IO
ポインタに何かが格納されていること
はわかりました。
(型)グロブとは
そもそもグロブとは何でしょうか。実用 Perlプログラミング 第2版 の 1.1.1 節に載っているのですが,ここでも簡単に説明します。
Perl にはさまざまな「型」($
スカラー,@
配列,%
ハッシュ,等々)の変数がありますが,それらは下記のようにフラットに格納されているわけではありません。
かわりに,変数の型識別子($
等)を取り除いた「名前」で表されるあらゆるものを示す「グロブ」を経由して格納されています。
グロブにはスカラーや配列などの実体へのポインタが存在します。これらをスロットと呼ぶことにします。今挙げたスカラー,配列のほか,関数(sub
)や I/O ハンドル(!)のスロットもあります。
ちなみに上記での foo
や bar
などの「名前」が格納されたテーブルを「シンボルテーブル」と呼びます*1。シンボルテーブルはパッケージの名前空間ごとに存在します。つまり,package main
の変数名・関数名等については main
用のシンボルテーブルがあり,package CGI
については CGI
用のシンボルテーブルがあり,といった具合です。
なぜこのような変態的な仕組みになっているのかは省略します((というか知らないんですが……おそらく,むかしむかしリファレンスというものがなく,*
によって「エイリアス」として表象していた頃の名残りだと思います。))。
グロブの IO
スロットをみてみる
いままでもったいつけてきましたが,いよいよ種明かしです。グロブの IO
スロットを見てみます。IO
スロットには *GLOB{IO}
のようなシンタックスでアクセスできます(詳しくは perlref をみてください)。
$handle
をグロブ(*
)でデリファレンスするには *$handle
と書けばいいのですが,見た目わかりやすくするため *{$handle}
でデリファレンスしてます((配列やハッシュのデリファレンスも @{$foo}
や %{$bar}
のように書けます。ぱっと見に人間パーサにわかりづらそうな場合,わたしはこのような記述をしています))。
use strict; use warnings; use Scalar::Util qw( blessed ); use Data::Dumper; use IO::Handle; sub say { print join q{}, @_, "\n"; } open my $handle, '>', 'output.txt' or die "open: $!"; say *{$handle}{IO}; # IO::Handle=IO(0x8154624) say ref *{$handle}{IO}; # IO::Handle say defined blessed *{$handle}{IO} ? blessed *{$handle}{IO} : 'unblessed'; # IO::Handle say Dumper(*{$handle}{IO}); # WARN: cannot handle ref type 15 at Data/Dumper.pm line 179. # $VAR1 = bless( , 'IO::Handle' ); say *{$handle}{IO}->isa('IO::Handle') ? 'isa IO::Handle' : 'not IO::Handle'; # isa IO::Handle say *{$handle}{IO}->can('close') ? 'can close' : 'cannot close' ; # can close $handle->close();
[http://search.cpan.org/perldoc?Data::Dumper:title=Data::Dumper]
に少し怒られていますが,やっと [http://search.cpan.org/perldoc?IO::Handle:title=IO::Handle]
との関わりを見つけることができました。
そもそも [http://search.cpan.org/perldoc?IO::Handle:title=IO::Handle]
との関連付けはどこでおこなわれているのか
*{$handle}{IO}
に格納されていたファイルハンドルの実体は Perl_newIO()
という関数で生成されます。
gv.c
からの抜粋です。
IO * Perl_newIO(pTHX) { GV *iogv; IO * const io = (IO*)NEWSV(0,0); sv_upgrade((SV *)io,SVt_PVIO); /* This used to read SvREFCNT(io) = 1; It's not clear why the reference count needed an explicit reset. NWC */ assert (SvREFCNT(io) == 1); SvOBJECT_on(io); /* Clear the stashcache because a new IO could overrule a package name */ hv_clear(PL_stashcache); iogv = gv_fetchpv("FileHandle::", FALSE, SVt_PVHV); /* unless exists($main::{FileHandle}) and defined(%main::FileHandle::) */ if (!(iogv && GvHV(iogv) && HvARRAY(GvHV(iogv)))) iogv = gv_fetchpv("IO::Handle::", TRUE, SVt_PVHV); SvSTASH_set(io, (HV*)SvREFCNT_inc(GvHV(iogv))); return io; }
おおまかにいうと,
- 新しい
SV
(この時点では汎用的な入れ物と思ってください)を生成し - それを
IO
ポインタタイプのものとし OBJECT
フラグを立てIO::Handle::
のシンボルテーブルを STASH プロパティに登録
しています。実は後者2つは [http://search.cpan.org/perldoc?IO::Handle:title=IO::Handle]
に bless
しているのに等しい操作です。
より上位では,このようにして得られた I/O ハンドルをグロブの IO
スロットに代入しています。
余談: FileHandle
について
上記のコードを見ると,[http://search.cpan.org/perldoc?FileHandle:title=FileHandle]
モジュールが読み込まれている場合,[http://search.cpan.org/perldoc?IO::Handle:title=IO::Handle]
ではなく [http://search.cpan.org/perldoc?FileHandle:title=FileHandle]
に関連付けされるようですが……
use strict; use warnings; #use IO::Handle; use FileHandle; open my $handle, '>', 'output.txt' or die "open: $!"; print *{$handle}{IO}, "\n"; # FileHandle=IO(0x8154624) close $handle;
おお,たしかに FileHandle
インスタンスとして見なされています。
FileHandle
モジュールというのは(一番最初に引用したように)IO::*
モジュールが出現するまで利用されていた,ファイルハンドルでオブジェクトインタフェースを利用するためのモジュールです。後方互換性のために残されていますが,レガシーモジュールです。
なぜ IO
スロット経由でメソッド呼び出しができたのか
一般的なオブジェクト(たとえばハッシュベースの場合 my $obj = bless {}, 'HTTP::Headers';
)の構造は下記のようになっています。
OBJECT
という flag が立っていて,あるシンボルテーブルが STASH
として登録されているとき,$obj->method()
のようにメソッドを呼び出すと,STASH
のシンボルテーブルからメソッドを探して実行します((むろんメソッドが見つからない場合 @ISA
をたどったり AUTOLOAD()
を呼び出したりなどの副次的な動作はありますが省略しています。))。
この状態と先ほどのファイルハンドルの構造をくらべると,ファイルハンドルの場合,GLOB
のぶん,レイヤが一枚増えていることになります。ということは,リファレンスがグロブであり,その IO
スロットが使用されている場合に,特別扱いをしているのではないかと推察されます。
実際にはどうでしょうか。pp_hot.c
から抜粋します。
STATIC SV * S_method_common(pTHX_ SV* meth, U32* hashp) { SV * const sv = *(PL_stack_base + TOPMARK + 1); /******* snip snip snip *******/ if (SvROK(sv)) ob = (SV*)SvRV(sv); else { /* IO グロブを操作しているので一見関係ありそうだが, 今回の話では関係がないので snip. */ } /* if we got here, ob should be a reference or a glob */ if (!ob || !(SvOBJECT(ob) || (SvTYPE(ob) == SVt_PVGV && (ob = (SV*)GvIO((GV*)ob)) && SvOBJECT(ob)))) { Perl_croak(aTHX_ "Can't call method \"%s\" on unblessed reference", name); } stash = SvSTASH(ob); fetch: /******* snip snip snip *******/ gv = gv_fetchmethod(stash ? stash : (HV*)packsv, name); /******* snip snip snip *******/ }
ざっくりと省略していますが,それでも長いですね。「特別扱い」という意味で特に重要なのは下記の部分です。
if (!ob || !(SvOBJECT(ob)
|| (SvTYPE(ob) == SVt_PVGV && (ob = (SV*)GvIO((GV*)ob))
&& SvOBJECT(ob))))
リファレンスの先に実体が存在して,それがオブジェクトではなく,タイプが GLOB
の場合に,GLOB
の IO
スロットを取り出して(via GvIO()
)あらたにオブジェクトとして先の処理に進んでいます*2。
たしかに「特別扱い」していました。
[http://search.cpan.org/perldoc?IO::File:title=IO::File]
を使うと
[http://search.cpan.org/perldoc?IO::File:title=IO::File]
を使ってファイルハンドル(オブジェクト)を生成した場合のインスタンスの構造を [http://search.cpan.org/perldoc?Devel::Peek:title=Devel::Peek]
で調べてみましょう。
use strict; use warnings; use Devel::Peek; use IO::File; my $handle = IO::File->new('output.txt', 'w') or die "open: $!"; print Dump($handle); $handle->close();
これの出力は,
SV = RV(0x81a8be8) at 0x81546cc REFCNT = 1 FLAGS = (PADBUSY,PADMY,ROK) RV = 0x81951e0 SV = PVGV(0x8213978) at 0x81951e0 REFCNT = 1 FLAGS = (OBJECT,GMG,SMG,MULTI) IV = 0 NV = 0 MAGIC = 0x82139e8 MG_VIRTUAL = &PL_vtbl_glob MG_TYPE = PERL_MAGIC_glob(*) MG_OBJ = 0x81951e0 STASH = 0x819539c "IO::File" NAME = "GEN0" NAMELEN = 4 GvSTASH = 0x8190858 "Symbol" GP = 0x82139b0 SV = 0x81a7c44 REFCNT = 1 IO = 0x81a7b6c FORM = 0x0 AV = 0x0 HV = 0x0 CV = 0x0 CVGEN = 0x0 GPFLAGS = 0x0 LINE = 24 FILE = "/usr/share/perl/5.8/Symbol.pm" FLAGS = 0x2 EGV = 0x81951e0 "GEN0"
GV
自体が [http://search.cpan.org/perldoc?IO::File:title=IO::File]
と bless
されていますが,基本的な構造は素のファイルハンドルとほぼ同じであることが分かります((ほか Symbol
の gensym()
関数でファイルハンドルシンボルを生成しているなどの違いもあります。))。
なぜ以上に述べたような不思議なしくみになっているのか
これまで述べてきたのと逆に [http://search.cpan.org/perldoc?IO::Handle:title=IO::Handle]
なインスタンスを古典的な I/O 関数(getc()
なり seek
なり)に引数として透過的に渡すためにこのようになっているのではないかなぁ。
たとえば文字列スカラをラッピングしたクラス([http://search.cpan.org/perldoc?URI:title=URI]
など)では ""()
を overload
することで透過的に渡すことができますが,I/O グロブの場合そのような関数(やコンテキスト)はありませんから。
まぁ,あくまで推測です。
おまけ: シンボルテーブルを網羅するには
[http://search.cpan.org/perldoc?Devel::Symdump:title=Devel::Symdump]
というモジュールを使うと,ある名前空間のシンボルテーブルを調べることができます。
use strict; use warnings; use Devel::Symdump; use IO::Handle; print Devel::Symdump->new(qw( IO::Handle ))->as_string, "\n";
の出力は,
arrays IO::Handle::EXPORT IO::Handle::EXPORT_FAIL IO::Handle::EXPORT_OK IO::Handle::ISA functions IO::Handle::DESTROY IO::Handle::SEEK_CUR IO::Handle::SEEK_END IO::Handle::SEEK_SET IO::Handle::_IOFBF IO::Handle::_IOLBF IO::Handle::_IONBF IO::Handle::_open_mode_string IO::Handle::autoflush IO::Handle::blocking IO::Handle::carp ...... snip snip snip ...... hashes ios packages scalars IO::Handle::BEGIN ...... snip snip snip ...... unknowns
のようになります。もちろん stringify 以外にもいろいろなプロパティを得ることができます。
おまけ: ベンチマーク
ベンチマークをとってみました。
#!/usr/bin/perl use strict; use warnings; use Benchmark qw(:all); use IO qw( File Handle ); our $SUBLOOP = 1000; timethese(1000, { bare => \&bare_handle, scal => \&via_scalar, damian => \&damian_way, io_handle => \&io_handle, io_file => \&io_file, }); exit; sub bare_handle { open HANDLE, '>', 'output.txt'; for (1 .. $SUBLOOP) { print HANDLE "HoHoHo!\n"; seek HANDLE, 0, 0; truncate HANDLE, 0; } close HANDLE; } sub via_scalar { open my $handle, '>', 'output.txt'; for (1 .. $SUBLOOP) { print $handle "HoHoHo!\n"; seek $handle, 0, 0; truncate $handle, 0; } close $handle; } sub damian_way { open my $handle, '>', 'output.txt'; for (1 .. $SUBLOOP) { print {$handle} "HoHoHo!\n"; seek $handle, 0, 0; truncate $handle, 0; } close $handle; } sub io_handle { open my $handle, '>', 'output.txt'; for (1 .. $SUBLOOP) { $handle->print("HoHoHo!\n"); #$handle->seek(0, 0); # OUCH! seek $handle, 0, 0; $handle->truncate(0); } $handle->close(); } sub io_file { my $handle = IO::File->new('output.txt', 'w'); for (1 .. $SUBLOOP) { $handle->print("HoHoHo!\n"); $handle->seek(0, 0); $handle->truncate(0); } $handle->close(); }
damian というのは PBP で推奨されている print {$handle} ...
というブロック記法*3のペナルティを調べるためです。また,[http://search.cpan.org/perldoc?IO::Handle:title=IO::Handle]
自身は [http://search.cpan.org/perldoc?IO::Seekable:title=IO::Seekable]
でないので,io_handle の seek()
のとこだけ通常の関数呼び出しを行っています(やっぱ素直に [http://search.cpan.org/perldoc?IO::File:title=IO::File]
を使うべきということですね)。
してその結果は下記の通り。
Benchmark: timing 1000 iterations of bare, damian, io_file, io_handle, scal... bare: 15 wallclock secs ( 2.35 usr + 11.81 sys = 14.16 CPU) @ 70.62/s (n=1000) scal: 13 wallclock secs ( 1.83 usr + 11.43 sys = 13.26 CPU) @ 75.41/s (n=1000) damian: 13 wallclock secs ( 1.84 usr + 11.44 sys = 13.28 CPU) @ 75.30/s (n=1000) io_handle: 16 wallclock secs ( 3.97 usr + 11.84 sys = 15.81 CPU) @ 63.25/s (n=1000) io_file: 16 wallclock secs ( 4.46 usr + 11.94 sys = 16.40 CPU) @ 60.98/s (n=1000)
純粋な Perl 部分の実行時間は,この場合 call per sec や wallclock secs より usr に注目するべきでしょう(ですよね?)。その上で,上記の結果からいえることは
- ベアワードより未定義スカラ変数を利用したほうが速い*4
- Damian 記法によるペナルティはない
- オブジェクトメソッド形式だと遅くなる
[http://search.cpan.org/perldoc?IO::File:title=IO::File]
を使うとより遅い(ちょっぴり速くなるのではと予想していたけど[http://search.cpan.org/perldoc?IO::File:title=IO::File]
から[http://search.cpan.org/perldoc?IO::Handle:title=IO::Handle]
に辿るぶん不利なのかも)- なんのかんのいってもカーネル/I/O に占める時間のほうが大きいので気にする必要はない
まとめ
- Perl には GLOB の IO スロットを経由してメソッドを呼び出す機構も存在する
- このため
open()
関数で取得したファイルハンドルを[http://search.cpan.org/perldoc?IO::Handle:title=IO::Handle]
パッケージのインスタンスとみなして処理を行うことができる - しかし
UNIVERSAL::isa('IO::Handle')
判定が false になるのでおすすめできない - オブジェクトインタフェースを使いたい場合は素直に
[http://search.cpan.org/perldoc?IO::File:title=IO::File]
を利用した方がよい
- 作者: Simon Cozens,菅野良二
- 出版社/メーカー: オライリージャパン
- 発売日: 2006/03/01
- メディア: 大型本
- 購入: 3人 クリック: 22回
- この商品を含むブログ (39件) を見る
「実用」というわりに実用的ではない気がします。どちらかというと,こういうこともできるのねーという「応用」かも。そういう意味ではクックブックのほうが「実用」的。
むかし本屋で一読したときは後半の specific な内容に嫌気がさして敬遠してしまったのですが,Perl について一通り学んだあとで再び目にする機会があり,後半も含めて気に入ったのでついに買ってしまいました。まぁ結構前に出版された本なので「モダン」じゃないモジュールが取り上げられたりもしてますが,それらの「実用」を目指すのでなければ読み物としておもしろいです。つかまだ通読できてない。
あーちなみに率直にいって翻訳の質がアレです。