Perl (5.8) での文字列の内部表象について返信

UTF8 フラグあれこれ - daily dayflower について nobuoka さんよりツッコミをいただきました。

nobuoka 2008/03/11 21:15
こんにちは。”[Perl] PerlUnicode 対応について” のエントリでトラックバックさせて頂きました nobuoka です。

内部表象 (内部形式: internal format) について気になる点があったのでいろいろ調べていたのですが、「内部形式は UTF-8 ではなく Unicode コードポイントをバイナリ化したものである」という結論に達しました。たとえば「é」という文字は内部形式では ¥xE9 というバイナリデータとして保持されているという結論に達しました。それは utf8 フラグが付いていても付いていなくても同様です。
つまり、このエントリで述べられている
(A) 文字列(内部表象: UTF-8
(B) 文字列(内部表象: ISO-8859-1)
ですが、utf8 フラグが付いているかいないかの違いだけで内部的なデータは同じもの ((B) は U+0000 〜 U+00FF の範囲のみですが) だと思うのです。だからこそ eq 演算子の比較でもきちんと比較されるのではないか、と。

詳しいことは
http://www.r-definition.com/program/perl/internalformat.htm
に書いてますのでよければまた目を通してください。
一般的に内部形式は UTF-8 だと言われてますし perldoc にもそんなことを書いてるのであんまり自信がないのですが。。

http://d.hatena.ne.jp/dayflower/20080219/1203493616#c1205237703

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";
# => 1
UTF8 フラグあれこれ - 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 に変換する
  • PerlIO レイヤが PERLIO_F_UTF8 フラグなしの場合(:bytes など)
    • 文字列が UTF8 flag つきの場合((かつ,use bytes プラグマ下ではない場合,です。)),utf8::downgrade() により,文字列表象を UTF-8 ⇒ Latin-1 に変換する
    • Wide character (>= U+0100) が含まれる場合 warning を出力する
結論

PERLIO_F_UTF8 フラグのないレイヤ上の PerlIO ストリームを通るとき,UTF8 flag つきで U+0000U+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* テンプレートにたいして,

  • Perl-5.8.8 ではとくに処理は行っていないこと
  • Perl-5.10.0 では UTF8 flag つきの場合 uni_to_byte() により bytes に変換していること

がわかります。nobuoka さんの考察とは逆になりました。

結論

Perl 5.8 では unpack('H*') は文字列の内部表象をそのまま出力している。Perl 5.10 では UCS-x に変換して出力する?

uni_to_byte() とか utf8 変数のあたり,もうちょっと深追いが必要。

まとめ

  • Perl の文字列の内部表象は UTF8 flag がついているかどうかでかわる
    • UTF8 flag ありの場合,UTF-8
    • UTF8 flag なしの場合,Latin-1(「文字列」の場合,そうみなされる,ということ)
  • UTF8 flag なしの文字列を utf8::upgrade() すると,UTF8 flag つきに変わり内部表象は UTF-8 となる
  • Perl 5.10 では pack / unpack の仕様変更など,いくつか改変されており,このことによりプログラマは文字列の内部表象について,より意識しなくてよくなっている