Perl (5.8) での文字列の内部表象について返信
UTF8 フラグあれこれ - daily dayflower について nobuoka さんよりツッコミをいただきました。
nobuoka 2008/03/11 21:15
こんにちは。”[Perl] Perl の Unicode 対応について” のエントリでトラックバックさせて頂きました nobuoka です。内部表象 (内部形式: internal format) について気になる点があったのでいろいろ調べていたのですが、「内部形式は UTF-8 ではなく Unicode コードポイントをバイナリ化したものである」という結論に達しました。たとえば「é」という文字は内部形式では ¥xE9 というバイナリデータとして保持されているという結論に達しました。それは utf8 フラグが付いていても付いていなくても同様です。
つまり、このエントリで述べられている
(A) 文字列(内部表象: UTF-8)
(B) 文字列(内部表象: ISO-8859-1)
ですが、utf8 フラグが付いているかいないかの違いだけで内部的なデータは同じもの ((B) は U+0000 〜 U+00FF の範囲のみですが) だと思うのです。だからこそ eq 演算子の比較でもきちんと比較されるのではないか、と。詳しいことは
http://d.hatena.ne.jp/dayflower/20080219/1203493616#c1205237703
http://www.r-definition.com/program/perl/internalformat.htm
に書いてますのでよければまた目を通してください。
一般的に内部形式は UTF-8 だと言われてますし perldoc にもそんなことを書いてるのであんまり自信がないのですが。。
nobuoka さんによると,UTF8 flag がついているいないに関わらず内部表象は UCS-x であるようだ,と。これはおもしろそうな説なのでやや深追いしてみました。
なお,基本的に Perl 5.8.8 のソースを元に議論します。また,EBCDIC システムについては取扱いません。
内部表象および utf8::upgrade
について
nobuoka さん曰く
よく、「Perl の内部では文字列は UTF-8 として扱われる」という記述を Web 上で見かけます(perldoc でもそう書いてる?)が、おそらくそれは正しくありません。少なくとも、U+0080 〜 U+00FF の文字に関しては UTF-8 では扱われていません。
Perl の内部形式に関する考察
世間一般はよくわかりませんが,私がいいたかったのは,
- 『文字列』の内部表象には2通りある
- UTF8 flag つき,UTF-8 で表象されている
- UTF8 flag なし,Latin-1 で表象されている,とみなされる
です。ともかく,nobuoka さんの説ではこれは間違いで
内部形式は、バイナリ構造で Unicode のコードポイントを保持しているようです。
[Perl] Perl の内部形式に関する調査:記:So-net blog
とのこと
文字列の内部表象(dayflower 説)についておさらいします。
#!/usr/bin/perl use strict; use warnings; my $str1 = "H\x{e9}llo"; my $str2 = $str1; utf8::upgrade($str2); # Now, $str1 is internally represented in Latin-1 encoding # $str2 is internally represented in UTF-8 encoding # $str1: {utf8=0}[H][\xe9] [e][l][l][o] # $str2: {utf8=1}[H][\xc3,\xa9][e][l][l][o] # 内的エンコーディングが違うだけで両者の意味するものは同じ print $str1 eq $str2 ? 1:0, "\n"; # => 1UTF8 フラグあれこれ - daily dayflower
では utf8::upgrade()
というのは何をしているのか。内部表象を変えずに UTF8 flag を立てているだけなのか?
該当部分を sv.c
より抜粋します。
STRLEN Perl_sv_utf8_upgrade_flags(pTHX_ register SV *sv, I32 flags) { /* ...... snip, snip, snip! ...... */ if (PL_encoding && !(flags & SV_UTF8_NO_ENCODING)) sv_recode_to_utf8(sv, PL_encoding); else { /* Assume Latin-1/EBCDIC */ /* This function could be much more efficient if we * had a FLAG in SVs to signal if there are any hibit * chars in the PV. Given that there isn't such a flag * make the loop as fast as possible. */ const U8 *s = (U8 *) SvPVX_const(sv); const U8 *e = (U8 *) SvEND(sv); const U8 *t = s; int hibit = 0; while (t < e) { const U8 ch = *t++; if ((hibit = !NATIVE_IS_INVARIANT(ch))) break; } if (hibit) { STRLEN len = SvCUR(sv) + 1; /* Plus the \0 */ U8 * const recoded = bytes_to_utf8((U8*)s, &len); SvPV_free(sv); /* No longer using what was there before. */ SvPV_set(sv, (char*)recoded); SvCUR_set(sv, len - 1); SvLEN_set(sv, len); /* No longer know the real size. */ } /* Mark as UTF-8 even if no hibit - saves scanning loop */ SvUTF8_on(sv); } return SvCUR(sv); }
0x80 以上の文字が含まれる文字列については,bytes_to_utf8()
((utf8.c
参照)) 関数を呼んでいます。ここで,Latin-1(擬似的には UCS-1)を UTF-8 に変換しているんですね。
結論
UTF8 flag つきの文字列の内部表象は UTF-8 である。
:bytes
レイヤで print
しても内部表象は得られない
一部プログラムを改変しつつ引用します。
以下の 2 つのプログラムを実行してみてください。上の方は、
binmode
によって「文字」を UTF-8 エンコードして出力するように、下の方は内部のバイナリデータをそのまま出力するように指定しています。...... snip, 上の例は略 ......
use utf8; open my $file, '>', 'internal_b.txt'; binmode $file, ':bytes'; my $str1 = "\xD7"; print {$file} "$str1: is utf8?", utf8::is_utf8($str1)? 1:0, "\n"; my $str3 = "×"; # ← \xC3, \x97 print {$file} "$str3: is utf8?", utf8::is_utf8($str3)? 1:0, "\n"; close $file;Perl の内部形式に関する考察
このプログラムを実行すると,たしかに1行目は utf8 = 0, 2行目は utf8 = 1 になりつつ,両者とも「×」として「\xD7」の表象の出力が得られます。
このことから,一見,UTF8 flag ありなしに関わらず,U+00D7
は内部表象として \xD7
という数値で表されているように見えます。ですが,これは PerlIO の print
で変換されているのです。
doio.c
より引用します。
bool Perl_do_print(pTHX_ register SV *sv, PerlIO *fp) { register const char *tmps; STRLEN len; /* ...... snip, snip, snip! ...... */ switch (SvTYPE(sv)) { /* ...... snip, snip, snip! ...... */ default: if (PerlIO_isutf8(fp)) { if (!SvUTF8(sv)) sv_utf8_upgrade_flags(sv = sv_mortalcopy(sv), SV_GMAGIC|SV_UTF8_NO_ENCODING); } else if (DO_UTF8(sv)) { if (!sv_utf8_downgrade((sv = sv_mortalcopy(sv)), TRUE) && ckWARN_d(WARN_UTF8)) { Perl_warner(aTHX_ packWARN(WARN_UTF8), "Wide character in print"); } } tmps = SvPV_const(sv, len); break; } /* ...... snip, snip, snip! ...... */
このコードは下記のロジックを示しています。
- PerlIO レイヤが
PERLIO_F_UTF8
フラグつきの場合(:utf8
など)- 文字列が UTF8 flag なしの場合,
utf8::upgrade()
により,文字列表象を Latin-1 ⇒ UTF-8 に変換する
- 文字列が UTF8 flag なしの場合,
- PerlIO レイヤが
PERLIO_F_UTF8
フラグなしの場合(:bytes
など)- 文字列が UTF8 flag つきの場合((かつ,
use bytes
プラグマ下ではない場合,です。)),utf8::downgrade()
により,文字列表象を UTF-8 ⇒ Latin-1 に変換する - Wide character (
>=
U+0100
) が含まれる場合 warning を出力する
- 文字列が UTF8 flag つきの場合((かつ,
結論
PERLIO_F_UTF8
フラグのないレイヤ上の PerlIO ストリームを通るとき,UTF8 flag つきで U+0000
〜 U+00FF
な文字で構成される文字列は Latin-1 に変換される。UTF8 flag つきで Wide character を含む文字列は warning を出しつつ内部表象のまま出力する(つまり結果的に UTF-8 octet stream となる)。
unpack
について
Perl 5.8 では、utf8 フラグつきのデータ (つまり「文字」の内部形式) に対して unpack を行う時、自動的に内部形式から UTF-8 エンコードに変換を行っていたのですが、Perl 5.10 では仕様が変わり、utf8 フラグは関係なく内部のバイナリデータを直接 unpack するようになったのです。
Perl の内部形式に関する考察
とのことですが,具体的に pp_pack.c
をみてみます。
まずは Perl 5.8.8 のソースから
STATIC I32 S_unpack_rec(pTHX_ register tempsym_t* symptr, register char *s, char *strbeg, char *strend, char **new_s ) { /* ...... snip, snip, snip! ...... */ case 'H': case 'h': /* ...... snip, snip, snip! ...... */ if (datumtype == 'h') { /* ...... snip, snip, snip! ...... */ } else { aint = len; for (len = 0; len < aint; len++) { if (len & 1) bits <<= 4; else bits = *s++; *str++ = PL_hexdigit[(bits >> 4) & 15]; } } *str = '\0'; XPUSHs(sv_2mortal(sv)); break; /* ...... snip, snip, snip! ...... */
次に Perl 5.10.0 のソースから
STATIC I32 S_unpack_rec(pTHX_ tempsym_t* symptr, const char *s, const char *strbeg, const char *strend, const char **new_s ) { /* ...... snip, snip, snip! ...... */ case 'H': case 'h': { /* ...... snip, snip, snip! ...... */ if (datumtype == 'h') { /* ...... snip, snip, snip! ...... */ } else { U8 bits = 0; const I32 ai32 = len; for (len = 0; len < ai32; len++) { if (len & 1) bits <<= 4; else if (utf8) { if (s >= strend) break; bits = uni_to_byte(aTHX_ &s, strend, datumtype); } else bits = *(U8 *) s++; *str++ = PL_hexdigit[(bits >> 4) & 15]; } } *str = '\0'; SvCUR_set(sv, str - SvPVX_const(sv)); XPUSHs(sv); break; } /* ...... snip, snip, snip! ...... */
以上のソースから,H*
テンプレートにたいして,
がわかります。nobuoka さんの考察とは逆になりました。