Perl の内部形式に関する考察 (誤りなので参考にしないでください)

この記事は誤っています。 Perl (5.8) での文字列の内部表象について返信 で引用して頂いたので残しているだけなので、参考にしないようにしてください。

正式版は コチラ です。

はじめに

重要なこととして、Perl 5.8 以降では、文字列 (text strings) とバイナリ列 (binary strings) は区別されるということがあります。バイナリとしてのデータはバイナリ列、文字列は文字列です。

文字、例えば「あ」という文字は「あ」という文字以外の何者でもありません。ですが、コンピュータ上で「あ」という文字が直接扱えるわけではない(コンピュータはデータをビット列として扱う)ので、文字をコンピュータが扱える形(バイナリデータ)にしなければありません。それがエンコードとよばれるもので様々なエンコード方式があります。同じ「あ」という文字でもエンコード方式によって異なるバイナリデータになってしまいます。「あ」という文字をエンコードすると

となります。

このように、同じ文字を表しているのにデータ(バイナリ)としては違うものになってしまっては、「文字」として扱うのが大変になります。そのため、Perl 5.8 では「文字」を「文字」として扱える機能が追加されました。(正確には Perl 5.6 からあったのですが、バグが多かったようです。)

Perl が扱う文字は Unicode と呼ばれる文字集合です。(ひらがなの 50 音表のようなもの。)Unicode の文字は、文字の並び順に対応する数字 (コードポイント) によって文字を表します。「あ」は Unicode の 12354 番目の文字なので、コードポイントは 12345 (0x3042) になります。"U+" に続けてコードポイントの 16 進数値を書くことで、その文字を表現することになっています。(つまり U+3042 は「あ」を表す。)
Perl で Unicode 文字をコードポイントで表現する場合は、\x{コードポイントの 16 進数値} という表現になります。

文字列 (text strings) とバイナリ列 (binary strings) の違いについては perlunitut が詳しいので参照してください。私が和訳した分はこちらです。

内部形式 internal format

文字を文字として扱うと言っても、先に述べたように、コンピュータは直接「文字」を扱えるわけではないので、Perl は、内部的にはバイナリデータとして文字列を扱います。この内部的なバイナリデータを内部形式 (internal format) と言います。

よく、「Perl の内部では文字列は UTF-8 として扱われる」という記述を Web 上で見かけます(perldoc でもそう書いてる?)が、おそらくそれは正しくありません。少なくとも、U+0080 ~ U+00FF の文字に関しては UTF-8 では扱われていません。内部形式がどのようなエンコード方式を用いているのかを知る必要はあまりありませんが、知っておけば役に立つと思うので書いておきます。

まず、内部形式というのは配列のようになっていて、1 byte を 1 文字として区切るのではなく 1 文字分の領域に自由に数 bytes を入れることが出来るようになっていると思われます。そしてどのような値がその 1 文字分の領域に入っているかが問題になるのですが、Unicode のコードポイントが直接入っているような気がします。

以下の 2 つのプログラムを実行してみてください。上の方は、binmode によって「文字」を UTF-8 エンコードして出力するように、下の方は内部のバイナリデータをそのまま出力するように指定しています。

use utf8;

open(STDOUT, "> internal_a.txt");
binmode STDOUT, ":utf8";
my $str1 = "\xD7";
print "$str1: is utf8?", utf8::is_utf8($str1)? 1:0, "\n";
my $str2 = "\x{00D7}";
print "$str2: is utf8?", utf8::is_utf8($str2)? 1:0, "\n";
my $str3 = "×";
print "$str3: is utf8?", utf8::is_utf8($str3)? 1:0, "\n";
use utf8;

open(STDOUT, "> internal_b.txt");
binmode STDOUT, ":bytes";
my $str1 = "\xD7";
print "$str1: is utf8?", utf8::is_utf8($str1)? 1:0, "\n";
my $str2 = "\x{00D7}";
print "$str2: is utf8?", utf8::is_utf8($str2)? 1:0, "\n";
my $str3 = "×";
print "$str3: is utf8?", utf8::is_utf8($str3)? 1:0, "\n";

同階層に "internal_a.txt" と "internal_b.txt" ができ、その内容はどちらも

×: is utf8?0
×: is utf8?0
×: is utf8?1

となっていると思います。ただし、"internal_a.txt" は UTF-8 エンコードで、"internal_b.txt" は Latin-1 エンコードになっているはずです。

'×' という文字は、Unicode では U+00D7 で表され、UTF-8 では C3 97 という 2 bytes です。\xD7 (= D7 という 1byte) と '×' という「文字」を出力した時に同じように出力されるということは、'×' という文字は内部的に D7 というバイナリデータとして扱われているということがわかります。

また、Latin-1 エンコードは、Unicode コードポイント (U+0000 ~ U+00FF の範囲) をバイナリ化したものと同じなので、binmode STDOUT, ":bytes"; したものが Latin-1 エンコードで '×' になるということからも、Perl の内部形式では Unicode のコードポイント (= Latin-1 の文字コード) が使われていることがわかります。

そのほかにも、U+0100 以上の「文字」をエンコードせずにそのまま出力すると「Wide character in print at ...」と警告されるのは有名ですが、U+00FF 以下の文字でこの警告が出ないのは、U+00FF 以下の文字が内部的に 1 byte であるから、と考えると納得がいくと思います。最初にも言いましたが、少なくとも U+0080 ~ U+00FF の範囲の文字は内部的に UTF-8 では扱われていないのです。
(内部的に UTF-8 ならば 2 bytes となり、"Wide character ..." と警告されるはず。)

ただ、Perl の内部形式は環境によって変わるようなのであまり内部形式を気にするのもよくないでしょう。互換性のために ASCII / Latin-1 環境では「文字」の内部形式が 1 byte のものと Latin-1 が対応するように、EBCDIC 環境では同様に「文字」の内部形式が 1 byte のものと EBCDIC が対応するようになっているようです。(perldoc を見た感じでは。)

pack / unpack 関数

ではなぜ、Perl の内部形式は UTF-8 だと思われているのでしょうか。それは、Perl 5.8 では utf8 フラグつきの文字に対して pack / unpack 関数を行う場合に、自動的に内部形式から UTF-8 エンコードに変換していたためです。

use utf8;

open(STDOUT, "> unpack.txt");
binmode STDOUT, ":utf8";
my $str1 = "\xD7";
print "$str1: is utf8?", utf8::is_utf8($str1)? 1:0,
    ", unpack(H*)", unpack("H*", $str1), "\n";
my $str3 = "×";
print "$str3: is utf8?", utf8::is_utf8($str3)? 1:0,
    ", unpack(H*)", unpack("H*", $str3), "\n";

上記のプログラムを実行してみてください。あなたが使っている Perl が 5.8 ならば、"unpack.txt" は以下のようになっているはずです。

×: is utf8?0, unpack(H*)d7
×: is utf8?1, unpack(H*)c397

utf8 フラグつきの '×' を template 'H*' で unpack すると、C397 になった、ということは '×' という「文字」は内部では C3 97 という 2 bytes で扱われているように見えます。ですが、それは違います。Perl 5.10 で同様のプログラムを実行すると、

×: is utf8?0, unpack(H*)d7
×: is utf8?1, unpack(H*)d7

となるのです。先に述べたように Perl 5.8 では、utf8 フラグつきのデータ (つまり「文字」の内部形式) に対して unpack を行う時、自動的に内部形式から UTF-8 エンコードに変換を行っていたのですが、Perl 5.10 では仕様が変わり、utf8 フラグは関係なく内部のバイナリデータを直接 unpack するようになったのです。

このような Perl 5.8 の仕様のため Perl の内部形式は UTF-8 だと思われているようですが、実際は違うのです。

なお、Perl 5.10 の pack / unpack 関数には character mode (C0 mode: 1 文字ごとに処理するモード) と UTF-8 mode (U0 mode: UTF-8 エンコードしたものの 1 byte 単位で処理するモード) の 2 つがあり、C0 / U0 テンプレートで指定できます。U0 モードで動作させることで、Perl 5.10 でも Perl 5.8 と同様の動作になります。詳しくは perldoc を参照してください。
Perl 5.8 では、utf8 フラグがある場合は U0 モードで、ない場合は C0 モードで動作するように自動で処理していたようです。

そしてこの Perl 5.10 の unpack 関数で調べたところ、U+0100 以上の文字も、Perl 内部では Unicode コードポイントをバイナリ化したものであるという結論に達しました。

print 関数

最後に parint 関数で「文字」をエンコードせずに出力した場合の動作について。

先の節でも述べたように、U+0100 以上の「文字」をエンコードせずにそのまま出力しようとすると「Wide character in print at ...」と警告されます。警告されますが、出力自体はちゃんと UTF-8 エンコードされたバイナリ列 (binary strings) が出力されています。

このことからも内部形式 (internal format) は UTF-8 ではないかという考えが生まれそうなのですが、ここでも pack / unpack と同様 Perl の自動エンコードが行われているのです。

以下のプログラムを実行してみてください。コメントに書いてあるような処理が行われます。

use utf8;

open(STDOUT, "> print.txt");
binmode STDOUT, ":bytes";   # 内部のバイナリ構造そのままで出力する
my $byte = "\xD7";  # \xD7 というバイナリ構造
my $char = 'あ';    # 「あ」という文字. 内部形式 \x{3042}
print $byte, "\n";  # \xD7 というバイナリ構造が出力される
print $char, "\n";
  # 「あ」という文字はそのまま出力できないので UTF-8 エンコードされ
  # \xE3 \x81 \x82 が出力される
my $str = $byte . $char;
  # \xD7 + \x{3042}
print $str, "\n";
  # そのまま出力できない文字「あ」が含まれるので、自動で UTF-8 エンコードされる
  # このときエンコードは「あ」だけでなく変数全体に対して行うので
  # \xD7 も「×」を表す文字とみなされエンコードされ、\xC3 \x97 になる
$byte = substr($str, 0, 1);  # $str から頭の 1 文字 (= \xD7) を取り出す
print $byte, "\n";           # \xD7 が出力される

実際に出力されるファイルを、Latin-1 コードとして読むと最初と最後の行に '×' があり、UTF-8 コードとして読むと内側の 2 行がちゃんと読めるようになっているはずです。Latin-1 として読むと以下のように、

×
あ
×あ
×

UTF-8 として読むと以下のように表示されていると思います。? は表示できないことを表すもので、環境によって空白であったりします。

?
あ
×あ
?

つまり、print 関数の引数に U+0100 以上の文字が含まれている場合 (= "Wide character in print ..." と警告される場合) には、自動的に Perl が文字を UTF-8 エンコードしているのです。そのとき、utf8 フラグは考慮しないので、バイナリ構造体 (上の例では \xD7) も U+00D7 とみなして UTF-8 エンコードされるのです。

ややこしいかもしれませんが、基本的に print や unpack はバイナリ構造体に使うもの、という認識を持っていれば特に混乱することはないと思います。

まとめ

文字列とバイナリ列は異なる
Perl 5.8 以降では、文字列 (text strings) とバイナリ列 (binary strings) は区別される。
内部形式について
文字列は Perl 内部ではバイナリデータとして保持され、その値は Unicode のコードポイントである。(おそらく)
utf8 フラグ (utf8 flag)
utf8 フラグの有無は Perl 5.10 ではあまり気にする必要がない。プログラムを書く上で「文字列」を扱っているのか「バイナリ列」を扱っているのかをしっかりわかっていれば良い。
Perl 5.8 ではある程度 utf8 フラグの有無を気にしなければならない。U+0080 ~ U+00FF の範囲の「文字」をテンプレート 'H' で unpack する場合など。しかし、「文字」を扱っているならば、テンプレート 'U' で unpack することで問題はなくなる。
pack / unpack, print 関数について
pack / unpack 関数は Perl 5.8 と 5.10 で仕様が異なっている。
Perl 5.8 では、utf8 フラグ付きの「文字」に対して pack / unpack すると、自動的に UTF-8 エンコードされて処理されていた。
Perl 5.10 では、pack / unpack 時に UTF-8 エンコードする U0 モードとエンコードしない C0 モードがある。
pack / unpack 関数や print 関数はバイナリデータにのみ使用することが望ましい。
ただし、pack / unpack 関数の 'U' テンプレートは文字に使用することを前提としている。また、文字に対して使用できるテンプレートもある。

扱っているデータが「文字列」なのか「バイナリ列」かが問題となる場面があるが、自動判別するよりかはプログラマがしっかりと意識しているのが望ましいと思われる。「文字列」とエンコードされた「バイナリ列」両方を扱えるモジュールを作る場合でも、同じインターフェイスで対応するよりは「文字列」用と「バイナリ列」用のインターフェイスに分けたほうがよさそうである。

関連文書

perlunitut (和訳), perlunifaq, perlunicode, perluniintro, Encode