Perl Hackers Hub

第16回Perl内部構造の深遠に迫る(1)

本連載では第一線のPerlハッカーが回替わりで執筆していきます。今回はgfxこと藤吾郎さんで、テーマはPerlの内部構造です。

内部構造を覗く

Perlで開発をしていると、ときどきわかりにくい現象に遭遇することがあります。たとえば、あるデータをJSONJavaScript Object Notationにシリアライズするとき、数値としてシリアライズしてほしい値が文字列としてシリアライズされてしまう。あるいは、エンコーディングが正しいはずなのに文字化けが起きる。こんなときは、思いきってPerlの内部構造を覗くことで、何が起きているかを突き止めることができます。

本稿では、Perlの内部構造について解説します。言及するperlはバージョン5.16.0(2012年5月21日リリース)です。また、ターミナルのエンコーディングはUTF-8を想定しています。なお、Perl処理系はC言語で書かれているのですが、本稿においてはC言語の知識は必須ではありません。

perl─⁠─Perl処理系の実装

本題の前に、Perl処理系について少し触れておきます。Perl言語の処理系は事実上1つしかなく、この実装はすべて小文字で「perl」と書きます。本稿でも以降perlと書くときはこの実装のことを指します。perlは前述したようにCで書かれているソフトウェアです。大量のマクロによって慣れないと非常にわかりにくいのですが、一つ一つ読み解けば理解はできます。

もっとも、本稿ではソースコードをほとんど参照しませんし、実際のデバッグでもソースコードを参照しなければならないケースはまれです。それでも、知識として内部構造を知っていると、内部構造を調べるツールを利用して問題を追っていくことができるのです。

SV構造体とDevel::Peekの読み方

さて、最初のテーマはSV構造体[1]です。SV構造体とは、Perlの値の実体です。Perlの値はほとんどの場合、中身が数値であるとか文字列であるということを意識せずに使えますが、それはSV構造体がそのように定義されているからです。つまりSV構造体を調べれば、perlの値の挙動がすべてわかるというわけです。

JSONシリアライズ問題

SV構造体を調べる前に、まずSV構造体の実装に起因した問題を追ってみましょう。

リスト1のコードを実行してみます。これは、JSON::PPモジュール[2]を使ってPerlのデータ構造をJSONにシリアライズするコードです。

リスト1 buggy-json-serialize.pl
use strict;
use warnings;
use feature 'say';
use JSON::PP qw(encode_json);
$| = 1; # autoflush

my %data = (
    answer => 42,
);

say "the answer: $data{answer}"; # (a)

say encode_json(\%data);
# 実行結果:
# The answer: 42
# {"answer":"42"}

$| = 1はSTDOUTのautoushを有効にするもの。これがないと、STDOUTに出力するsay()とSTDERRに出力するDump()が正しい順番で表示されないことがあるため行っている

実行結果を見ると、JSONにシリアライズした"answer"の値が文字列の"42"になっています。JSONは数値と文字列を区別するフォーマットですから、数値として入れたものは数値としてシリアライズされなければ意図したとおりではありません。(a)の行をコメントアウトすると意図どおり数値にシリアライズされます。この問題はWeb APIのレスポンスとしてJSONを返すアプリケーションで起こりがちです。一般的には、数値として出力すべきものは0 +$data{answer}などとして確実に数値にしてからデータ構造を作り、その後すぐにシリアライズすることで回避できるとされています。

なぜこのような問題が起きるのでしょうか。⁠確実に数値にしてから」とはどういう意味なのでしょうか。そもそもPerlの値に数値や文字列といった区別はあるのでしょうか。あるとしたらどのように区別されるのでしょうか。この疑問に答えるために、SV構造体を調べる必要があるのです。

SV構造体の調べ方

perlの基本データ構造であるSVをPerlから見る方法は2つあります。Devel::Peekモジュールで調べる方法とBモジュールで調べる方法です。

Devel::Peekモジュールで調べる

まずDevel::Peekを以下のコマンドで使ってみましょう。

$ perl -MDevel::Peek -e 'Dump "foo"'

出力結果は図1のようになります。一見複雑ですが、すべての行に意味があります。

図1 Devel::Peek::Dump("foo")の実行結果
001: SV = PV(0x7fc474001ea0) at 0x7fc47402ac40
002: REFCNT = 1
003: FLAGS = (PADTMP,POK,READONLY,pPOK)
004: PV = 0x10c702af0 "foo"\0
005: CUR = 3
006: LEN = 16

1行目のSV = PV(0x...a0) at 0x...40は、このSVがPVであることを示しています。PVとはポインタ値Pointer Value注3の略で、テキスト文字列やバイト列のためのSV型です。0x...から始まる16進数の値はCレベルのアドレスで、それぞれPV(0x...a0)がSVボディのアドレス、at 0x...40がSVヘッダのアドレスです。SVボディとは、SVごとのデータを所有している構造体です。SVヘッダはリファレンスカウントやSVごとに共通するフラグを所有している構造体です。SVヘッダはSVボディのポインタ[4]も所有しています。

2行目のREFCNTはSVのリファレンスカウンタで、SVの参照が1であることを示しています。SVはリファレンスカウンティングGCGarbage Collectionと呼ばれるしくみで管理されており、たとえばPerlのオブジェクトで値が参照されなくなったときにすぐデストラクタが呼ばれるのは、GCの働きによるものです。このリファレンスカウンタはすべてのSVに必要であるため、SVヘッダが所有しています。

3行目のFLAGSもすべてのSVに必要なデータで、SVヘッダが所有しています。FLAGSには、SVの型やさまざまな属性が入っています。たとえばPADTEMPは値がリテラルであること、POKとpPOKは文字列として参照可能な状態にあること、READONLYは値が不変であることを意味しています[5]⁠。

4行目以降のフィールド、つまりPV、CUR、LENはPV系に固有のデータで、それぞれ文字列バッファ、文字列のバイナリ表現のサイズ、確保してあるメモリ領域のサイズを表しています。

Bモジュールで調べる

この文字列"foo"を表すSVを仮にPerlで表現すると、リスト2のようになるでしょう。BODYの部分はSVの型によってさまざまなデータが入ります。

リスト2 Perlで表現するSV構造
my $sv = bless {
    REFCNT => 1,
    FLAGS  => [qw(PADTMP POK READONLY pPOK)]
    BODY   => {
        PV  => "foo",
        CUR => 3,
        LEN => 16,
    },
}, "SV::PV";

Devel::PeekはSVの内容をテキストに整形してコンソールに出力するだけですが、PerlプログラムからこのようなインタフェースでSVにアクセスすることもできます。それがBモジュールです。リスト3はBモジュールでSVの構造を調べるプログラムです。

FLAGSが整数であることを除き、Devel::Peekとほぼ同じ情報が得られます。プログラムから操作するにはDevel::Peekよりも都合が良いのですが、FLAGSを名前で表示してくれる分デバッグにはDevel::Peekが使いやすいでしょう。本稿でも以降はDevel::Peekを使います。

リスト3 use-B.pl
use strict;
use warnings;
use feature 'say';
use B qw(svref_2object);

my $sv = svref_2object(\"foo");
say ref($sv);      # B::PV
say $sv->FLAGS; # 134235140
say $sv->PV;    # foo
say $sv->CUR;   # 3
say $sv->LEN;   # 16

改めてJSONシリアライズ問題を追う

これでJSONシリアライズ問題を追うことができるようなりました。問題の個所をDevel::Peekで見てみましょう。リスト4を見てください。これはリスト1に2ヵ所Dump()を加えたものです。

リスト4 buggy-json-serialize-with-devel-peek.pl
use strict;
use warnings;
use feature 'say';
use JSON::PP qw(encode_json);
use Devel::Peek;
$| = 1; # autoflush

my %data = (
    answer => 42,
);

Dump $data{answer}; # (a)
say "the answer: $data{answer}"; # (b)
Dump $data{answer}; # (c)

say encode_json(\%data);

結果は図2のようになります。$data{answer}に触れる前、つまり(a)の地点ではこのSVの型はIVでした(図2の1行目⁠⁠。IVとは整数値Integer Valueのことです。ところがリスト4(b)で$data{answer}に触れたあと、リスト4(c)の地点ではSVがPVIVになっています(図2の6行目⁠⁠。PVIVとは、PVとIV双方の性質を持つ値です。つまり、値にアクセスしただけでSVボディの中身が変わっています。SVヘッダは両方ともat0x7fccc3003ce8であり、変化はありません。

図2 リスト4の実行結果
001: SV = IV(0x7fccc3003cd8) at 0x7fccc3003ce8
002: REFCNT = 1
003: FLAGS = (IOK,pIOK)
004: IV = 42
005: the answer: 42
006: SV = PVIV(0x7fccc3038238) at 0x7fccc3003ce8
007: REFCNT = 1
008: FLAGS = (IOK,POK,pIOK,pPOK)
009: IV = 42
010: PV = 0x10c5b2ca0 "42"\0
011: CUR = 2
012: LEN = 16
013: {"answer":"42"}

このSVボディの変化がJSONシリアライズの結果に影響を与えているのでしょうか。

SVのアップグレード

このSVボディの変化はSVのアップグレードと呼ばれており、ほかのスクリプト言語には見られないperl(あるいはPerl)に特有の現象です。Perlの値は文字列として扱うと文字列として振る舞い、数値として扱うと数値として振る舞うという性質を持っています。つまり、ある値が文字列なのか数値なのかは、プログラマの判断に委ねられています。

しかしperlを実装しているC言語では、文字列と数値はまったく異なるデータです。このため、SV型の中身がCの数値であったとしても、Perlから文字列として参照したとき、自動的にCの数値からCの文字列を生成します。このとき、生成結果を再利用するために、

数値や文字列など複数の値の入るSVのボディを割り当てます。この操作がSVのアップグレードです。たとえば、リスト4(b)ではIVからPVIVへのアップグレードが起きています。

SVのアップグレードが起きると、以前の型が何だったのかを確かめる術はありません。リスト5を見てください。IVからPVIVへのアップグレードも、PVからPVIVへのアップグレードも、結果のPVIVだけを見ると各種アドレス値以外はまったく同じです。

リスト5 sv-upgrade.pl
use strict;
use warnings;
use Devel::Peek;
{
    my $sv = 10; # IV
    my $dummy = "$sv"; # upgrade to PVIV

    Dump $sv;
    # 実行結果(抜粋):
    # SV = PVIV(0x7fe1bc0381f0) at 0x7fe1bc02abb0
    # FLAGS = (PADMY,IOK,POK,pIOK,pPOK)
}
{
    my $sv = "10"; # PV
    my $dummy = int $sv; # upgrade to PVIV

    Dump $sv;
    # 実行結果(抜粋):
    # SV = PVIV(0x7fe1bc038208) at 0x7fe1bc02ac10
    # FLAGS = (PADMY,IOK,POK,pIOK,pPOK)
}

SVアップグレードが起きる条件は法則がないように見えます。たとえば、say "the answer: $data{answer}"はSVアップグレードが起きますが、say"the answer: ", $data{answer}ではSVアップグレードは起きません。実用上は値にアクセスすると常にSVアップグレードが起きる可能性があると考えてよいでしょう。

アップグレードの逆の操作

ところで、アップグレードしたものは元に戻せないのでしょうか。たとえば、PVIVをIVやPVに戻すことはできないのでしょうか。これは新しく値を作りなおすことで、そのとき必要なデータだけが格納される最も「小さい」SV型を得ることができます。たとえば、0+ $valueはIVないしNVNumber Valueを、"" .$valueはPVを得る操作です。

SV構造体まとめとJSONシリアライズ問題の真相

さてこのあたりで一度SVについてまとめましょう。

  • Perlの値の実装はSVというCのデータ構造体である
  • SVでは文字列と数値は異なるデータである
  • SVは必要に応じて自動的にアップグレードする
  • SVアップグレードは値を使うといつでも起きる可能性がある
  • 一度SVアップグレードが起きると、アップグレード前の型の情報は失われる
  • SVを生成しなおすことで、特定の内部データ型のSVを得られる

これでJSONシリアライズ問題の原因が見えてきました。どうやらこの問題は、⁠一度アップグレードが起きるとアップグレード前の型の情報が失われる」ということが関係しそうです。つまり、たとえばシリアライザがPVIVという値を見たとき、これを文字列としてシリアライズするか数値としてシリアライズするか決定する必要があります。しかし、SVの仕様上あるPVIVがもともとIVなのかPVなのかを知ることができません。

まさにこのような仕様のために、JSON::PPモジュールは、ある値がたとえ数値として作られたものであったとしても、文字列として解釈可能であれば常に文字列としてシリアライズするのです。そしてこれは、JSON::PPがもともとJSON::XSというXSモジュール、つまりCで実装されているperlのAPIを使ってSVにアクセスするモジュールであり、それゆえにperlの実装の都合が表に出てしまっていると言えるでしょう。

この問題が起きるのは、先述したようにデータ構造を構築し終わってからさらに値にアクセスしたときです。0 + $valueというイディオムは、確実にIVないし

NVを得るためのものだったというわけです。そして、一度シリアライズ用にデータ構造を構築したら、その値にはアクセスしてはいけないということもおわかりいただけたかと思います。

SV型とFLAGSの関係

ところで、これまでSVについてPVやIV、PVIVなどと言及し、これらの型がSVの持っている情報を表現しているかのように説明してきました。説明を簡単にするために詳細を省いていたのですが、実はSVの型とSVの所有しているデータは必ずしも対応していません。言い換えれば、SVの型がPVだからといって必ずしも有効な文字列ではないし、PVIVが有効な文字列と整数を両方格納しているとも限りません。SVの型がPVIVであるというのは、文字列も整数も格納できるという可能性を示しているだけです。

つまり、SVに関して見るべき情報は、SVの型ではなくFLAGSなのです。図2をもう一度見てください。3行目はFLAGS = (IOK,pIOK)となっています。このSVは、SVの型がIVだから整数値なのではなく、IOKフラグが立っているから整数値として有効なのです。SVの型をIVにしたまま、整数として無効な値にすることもできます。リスト6ではそのような値を作っています。

リスト6 iv-but-undef.pl
use strict;
use warnings;
use Devel::Peek;

my $s = 10;
$s = undef;
Dump $s;
# 実行結果(抜粋):
# SV = IV(0x7ff4ca02abe8) at 0x7ff4ca02abf8
#   FLAGS = (PADMY)
#   IV = 10

SVの型はIVのままで、IVフィールドにも10が入っていますが、FLAGSからはIOKが消えています。Perlコードを見ればわかるように、この変数の値はundefです。SVの内部にまで立ち入ってデバッグしているとき、SVの型に惑わされて正しい解釈ができないということは起こりがちです。しかし、意味があるのはSVの型ではなくFLAGSなのです。

ただし、SVが単純なスカラでない場合はこの限りではなく、SVの型に意味があります。配列の実体であるAVArray Valueは常に配列を意味しますし、ハッシュの実体であるHVHash ValueはFLAGSの値にかかわらず常にハッシュを意味します。型グロブのGVGlob ValueやサブルーチンのCVCode Valueも同様です。これらを識別するためにはSV型を見るのが正しいのです。

<続きの(2)こちらから。>

おすすめ記事

記事・ニュース一覧