ファイルハンドルをめぐる冒険(ただしマニア向け)

以下は Perl 5.8.8 のソースを元に記述しました。Perl 5.10 でもそう変わってはいないと思いますが,結構内部が変更されているので違うかもしれません。大まかには同じだと思います。

イントロダクション

Perl でのファイルハンドルは内部的には [http://search.cpan.org/perldoc?IO::Handle:title=IO::Handle] オブジェクトとして保持されています。そのような記述は perl5004delta くらいでしか見つかりませんでした。ラクダ本にもなかったような。

Internal 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}.

perl5004delta

ですから,実は [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 ハンドル(!)のスロットもあります。

ちなみに上記での foobar などの「名前」が格納されたテーブルを「シンボルテーブル」と呼びます*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] との関わりを見つけることができました。

結局のところ,$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;
}

おおまかにいうと,

  1. 新しい SV(この時点では汎用的な入れ物と思ってください)を生成し
  2. それを IO ポインタタイプのものとし
  3. OBJECT フラグを立て
  4. 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 の場合に,GLOBIO スロットを取り出して(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 されていますが,基本的な構造は素のファイルハンドルとほぼ同じであることが分かります((ほか Symbolgensym() 関数でファイルハンドルシンボルを生成しているなどの違いもあります。))。

なぜ以上に述べたような不思議なしくみになっているのか

これまで述べてきたのと逆に [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] を利用した方がよい

実用 Perlプログラミング 第2版

実用 Perlプログラミング 第2版

「実用」というわりに実用的ではない気がします。どちらかというと,こういうこともできるのねーという「応用」かも。そういう意味ではクックブックのほうが「実用」的。

むかし本屋で一読したときは後半の specific な内容に嫌気がさして敬遠してしまったのですが,Perl について一通り学んだあとで再び目にする機会があり,後半も含めて気に入ったのでついに買ってしまいました。まぁ結構前に出版された本なので「モダン」じゃないモジュールが取り上げられたりもしてますが,それらの「実用」を目指すのでなければ読み物としておもしろいです。つかまだ通読できてない。

あーちなみに率直にいって翻訳の質がアレです。

*1:内部的には STASH というのですが,これらの用語の使い分けについてはよく知りません。

*2:個人的な意見としては,このように条件式の中に副作用のある代入を行うのは見通しが悪くなるのでよくないコードだと思います。

*3:個人的には気に入っています。

*4:都度グローバルシンボルテーブルを捜索するからかも。