unpackと\xNNエスケープシーケンス

次のコードは(255)が返ってくると思ったんだけど、(195)が返ってきた。

$ gosh -u binary.pack
gosh> (unpack "C" :from-string "\xff")
(195)
gosh>

文字列をユニフォームベクタに変換してみると、やはり195と191になっている。

gosh> (use gauche.uvector)
#<undef>
gosh> (string->u8vector "\xff")
#u8(195 191)
gosh>

なぜだろう。リーダーが文字列を読んでいるのはsrc/read.cのread_stringだ。

static ScmObj read_string(ScmPort *port, int incompletep,
                      ScmReadContext *ctx)
{
int c = 0;
ScmDString ds;
Scm_DStringInit(&ds);

#define FETCH(var)                                      \
if (incompletep) { var = Scm_GetbUnsafe(port); }    \
else             { var = Scm_GetcUnsafe(port); }
#define ACCUMULATE(var)                                 \
if (incompletep) { SCM_DSTRING_PUTB(&ds, var); }    \
else             { SCM_DSTRING_PUTC(&ds, var); }
#define INTRALINE_WS(var)                               \
((var)==' ' || (var)=='\t' || SCM_CHAR_EXTRA_WHITESPACE_INTRALINE(var))

/* 中略 */
        case 'x': {
            int cc = read_string_xdigits(port, 2, 'x', incompletep);
            ACCUMULATE(cc);
            break;
        }
src/read.c - read_string

read_string_xdigitsで"x"以降の2バイトを16進数として読んでいるから、ccは255となって、それをACCUMULATEマクロに渡している。ここでリーダーが読んでいるのは不完全文字列ではないから、SCM_DSTRING_PUTCが呼ばれる。これはsrc/gauche/string.hで定義されたマクロで、ScmDString(文字列をチャンクのリストとして管理しているデータ構造。たぶん)に文字をひとつ追加する。

#define SCM_DSTRING_PUTC(dstr, ch)                      \
do {                                                \
    ScmChar ch_DSTR = (ch);                         \
    ScmDString *d_DSTR = (dstr);                    \
    int siz_DSTR = SCM_CHAR_NBYTES(ch_DSTR);        \
    if (d_DSTR->current + siz_DSTR > d_DSTR->end)   \
        Scm__DStringRealloc(d_DSTR, siz_DSTR);      \
    SCM_CHAR_PUT(d_DSTR->current, ch_DSTR);         \
    d_DSTR->current += siz_DSTR;                    \
    if (d_DSTR->length >= 0) d_DSTR->length++;      \
} while (0)
src/gauche/string.h - SCM_DSTRING_PUTC

この中で使われているSCM_CHAR_PUTマクロは、コンパイル時に指定した内部エンコーディングUTF-8ならsrc/gauche/char_utf_8.hに定義されたものが使われる。

#define SCM_CHAR_PUT(cp, ch)                            \
do {                                                \
    if (ch >= 0x80) {                               \
        Scm_CharUtf8Putc((unsigned char*)cp, ch);   \
    } else {                                        \
        *(cp) = (unsigned char)(ch);                \
    }                                               \
} while (0)
src/gauche/char_utf_8.h - SCM_CHAR_PUT

ここでchは255(ff)だから、Scm_CharUtf8Putcが呼ばれる。

void Scm_CharUtf8Putc(unsigned char *cp, ScmChar ch)
{
if (ch < 0x80) {
    *cp = (u_char)ch;
}
else if (ch < 0x800) {
    *cp++ = (u_char)((ch>>6)&0x1f) | 0xc0;
    *cp = (u_char)(ch&0x3f) | 0x80;
}
else if (ch < 0x10000) {
src/gauche/char_utf_8.h - Scm_CharUtf8Putc

0x80 < ch < 0x800だから2番目の条件式が成立して、chは2バイトを使ってUTF-8エンコードされる。UTF-8 - Wikipediaによると、2バイトでエンコードする場合は、1バイト目の上位3ビットと2バイト目の上位2ビットがフラグとして使われて、残りの11ビットにchのビットパターンが右詰めでセットされる。上記のコードはそれをやっていて、その結果エンコードされた2バイトのビットパターンは

11000011 10111111

となり、これを10進数で表せば

gosh> #b11000011
195
gosh> #b10111111
191

だから、冒頭のコードは"\xff"をUTF-8エンコードした結果の1バイト目を返していたということだった。

ここまで書いて気づいたけど、リファレンスにちゃんと書いてあったorz

\xNN

2桁の16進数NNで指定されるバイト。このバイトは内部エンコーディングによって解釈されます。
Gauche ユーザリファレンス: 6.11 文字列