はじめに

次のスクリプトがあり、文字コードはEUC-JPとします。

p "日本語の文字列".length
p "日本語の文字列"[0]
p "日本語の文字列"[4..-1]
p "日本語の文字列".index("文字")

普通にスクリプトを実行させると以下の結果が得られます。

$ ruby m17n_string.rb
14
"\xC6"
"\xB8\xEC\xA4\xCE\xCA\xB8\xBB\xFA\xCE\xF3"
8

"-E EUC-JP"付きでスクリプトを実行させると以下の結果が得られます。

$ ruby -E EUC-JP /home/junjis/tmp/m17n.rb 
7
"日"
"文字列"
4

これはRuby1.9でm17n対応が行われたためです。今回の読解対象はm17n対応がどう行われており従来のメソッドがどう変更されているかです。

エンコーディングの初期化

初期化のときも触れましたがエンコーディングはこっそり初期化されています。今回はちゃんと初期化の中身を見ることにします。

encoding.c

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
 
-
|
|
|
|
|
|
rb_enc_init(void)
{
    enc_table_count = enc_table_expand(ENCINDEX_BUILTIN_MAX);
#define ENC_REGISTER(enc) enc_register_at(ENCINDEX_##enc, rb_enc_name(ONIG_ENCODING_##enc), ONIG_ENCODING_##enc)
    ENC_REGISTER(ASCII);
    ENC_REGISTER(EUC_JP);
    ENC_REGISTER(SJIS);
    ENC_REGISTER(UTF8);

というわけでエンコーディング処理の実体は鬼車が行っているようです。ONIG_ENCODING_EUC_JPとかの定義は以下のようになっています。

oniguruma.h

Everything is expanded.Everything is shortened.
  1
  2
 
 
#define ONIG_ENCODING_EUC_JP       (&OnigEncodingEUC_JP)
ONIG_EXTERN OnigEncodingType OnigEncodingEUC_JP;

encoding.h

Everything is expanded.Everything is shortened.
  1
 
typedef OnigEncodingType rb_encoding;

enc/euc_jp.c

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
!
OnigEncodingDefine(euc_jp, EUC_JP) = {
  mbc_enc_len,
  "EUC-JP",   /* name */
  3,          /* max enc length */
  1,          /* min enc length */
  onigenc_is_mbc_newline_0x0a,
  mbc_to_code,
  code_to_mbclen,
  code_to_mbc,
  mbc_case_fold,
  onigenc_ascii_apply_all_case_fold,
  onigenc_ascii_get_case_fold_codes_by_str,
  property_name_to_ctype,
  is_code_ctype,
  get_ctype_code_range,
  left_adjust_char_head,
  is_allowed_reverse_match,
  0
};

regenc.h

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 
 
 
 
 
 
-
|
|
!
 
 
 
 
 
 
#ifdef ONIG_ENC_REGISTER
extern int ONIG_ENC_REGISTER(const char *, OnigEncodingType*);
#define OnigEncodingName(n) encoding_##n
#define OnigEncodingDeclare(n) static OnigEncodingType OnigEncodingName(n)
#define OnigEncodingDefine(f,n)                      \
    OnigEncodingDeclare(n);                          \
    void Init_##f(void) {                            \
        ONIG_ENC_REGISTER(OnigEncodingName(n).name,  \
                          &OnigEncodingName(n));     \
    }                                                \
    OnigEncodingDeclare(n)
#else
#define OnigEncodingName(n) OnigEncoding##n
#define OnigEncodingDeclare(n) OnigEncodingType OnigEncodingName(n)
#define OnigEncodingDefine(f,n) OnigEncodingDeclare(n)
#endif

見るとわかると思いますがEUC-JPみたいな組み込みのエンコーディングは下、ISO-8859-1みたいな後から読み込むエンコーディングは上が使われるようです。

OnigEncodingEUC_JPの各関数は時が来たら見るとして次にスクリプト解析時にどのようにエンコーディングが設定されているかを見ていきましょう。

スクリプト読み込み時のエンコーディング設定

まず、-Kオプションや-Eオプションが指定された場合の動きです。ただし、Ruby1.9では-Kオプションは非推奨らしいです。

ruby.c

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 
-
|
-
|
|
-
|
|
-
|
-
|
|
|
|
!
-
|
!
|
|
|
|
|
|
|
|
-
|
|
|
-
|
!
|
|
|
-
|
|
proc_options(int argc, char **argv, struct cmdline_options *opt)
{
    ...
    for (argc--, argv++; argc > 0; argc--, argv++) {
        ...
        s = argv[0] + 1;
        switch (*s) {
          ...
          case 'K':
            if (*++s) {
                rb_encoding *enc = 0;
                switch (*s) {
                  case 'E': case 'e':
                    enc = ONIG_ENCODING_EUC_JP;
                    break;
                  ...
                }
                if (enc) {
                    opt->enc_index = rb_enc_find_index(rb_enc_name(enc));
                }
          case 'E':
            ...
            goto encoding;
          ...
          case '-':
            ...
            s++;
            ...
            else if (strcmp("encoding", s) == 0) {
                int idx;
                ...
              encoding:
                if ((idx = rb_enc_find_index(s)) < 0) {
                    rb_raise(rb_eRuntimeError, "unknown encoding name - %s", s);
                }
                opt->enc_index = idx;
...
load_file(VALUE parser, const char *fname, int script, struct cmdline_options *opt)
{
    ...
    if (opt->enc_index >= 0) rb_enc_associate_index(f, opt->enc_index);

次にスクリプト解析時の動きです。

parse.y

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
 
-
|
|
yycompile0(VALUE arg, int tracing)
{
    parser->enc = rb_enc_get(lex_input);
...

これでスクリプトを読み込むときにどの文字コードを使うかが設定されました。 続いてスクリプトに埋め込まれているエンコーディング指定を処理する部分を見てみましょう。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 
-
|
-
|
-
|
-
-
|
!
!
!
parser_yylex(struct parser_params *parser)
{
    ...
    switch (c = nextc()) {
      case '#':        /* it's a comment */
        if (!parser->has_shebang || parser->line_count != 1) {
            /* no magic_comment in shebang line */
            if (!parser_magic_comment(parser, lex_p, lex_pend - lex_p)) {
                if (parser->line_count == (parser->has_shebang ? 2 : 1)) {
                    set_file_encoding(parser, lex_p, lex_pend);
                }
            }
        }

parser_magic_comment関数ではまずmagic_comment_marker関数を使って-*-に囲まれた部分を抜き出しています。lex_pは#の一つ先を指しているため、parser_magic_comment関数に渡されるのは#の次のスペースの部分から改行までになります。

 ↓lex_p                    ↓lex_pend
# -*- encoding: EUC-JP -*-\n
     ↑beg                ↑end

次にヘッダ部分と値部分にポインタがセットされます。

              ↓end   ↓vend
# -*- encoding: EUC-JP -*-\n
      ↑beg     ↑vbeg

その後、ヘッダ部とmagic_comments構造体のnameを比較し、一致した構造体に登録されている関数にディスパッチされています。今のところ、magic_comment_encodingだけのようですが。ともかくこれでparser->encにエンコーディングが設定されます。

Stringへのエンコーディング設定

文字列の読み込みはparser_parse_string関数で行われます。注目部分だけ抜き出すと以下の通り。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 
-
|
|
|
|
-
|
|
parser_parse_string(struct parser_params *parser, NODE *quote)
{
    ...
    rb_encoding *enc = parser->enc;
    ...
    if (tokadd_string(func, term, paren, "e->nd_nest,
                      &enc) == -1) {
    ...
    set_yylval_str(STR_NEW3(tok(), toklen(), enc, func));

parser_tokadd_string関数はいろいろやっていますがとりあえずは以下を見ておけばよいでしょう。って、上のコードはtokadd_stringなのになんでparser_tokadd_stringの話をしているかについてはスクリプト解析を読むを参照してください。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 
 
 
-
|
-
|
-
|
|
-
|
|
|
|
|
|
-
|
!
|
|
-
|
-
|
|
!
|
|
!
|
|
!
|
parser_tokadd_string(struct parser_params *parser,
             int func, int term, int paren, long *nest,
             rb_encoding **encp)
{
    ...
    while ((c = nextc()) != -1) {
        ...
        else if (c == '\\') {
            ...
            c = nextc();
            switch (c) {
              ...
              case 'u':
                ...
                parser_tokadd_utf8(parser, &enc, 1,
                                   func & STR_FUNC_SYMBOL,
                                   func & STR_FUNC_REGEXP);
                if (has_nonascii && enc != *encp) {
                    mixed_escape(beg, enc, *encp);
                }
                continue;
        ...
        else if (!parser_isascii()) {
            has_nonascii = 1;
            if (enc != *encp) {
                mixed_error(enc, *encp);
                continue;
            }
            if (tokadd_mbchar(c) == -1) return -1;
            continue;
        }
        ...
        tokadd(c);
    }
    *encp = enc;

どういう動きかをしているかというと以下のような動きをしています。

  • \uXXXX形式のバックスラッシュ記法があると文字列のエンコーディングがUTF-8になり、呼び出し元にも反映される
  • 複数のエンコーディングが混ざっている、例えば、EUC-JPとしているスクリプトに"あ\u1234"と書かれているとエラーになる

次の注目対象はparser_tokadd_mbchar関数です。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 
-
|
-
|
|
!
|
|
|
|
!
 
 
parser_tokadd_mbchar(struct parser_params *parser, int c)
{
    int len = parser_precise_mbclen();
    if (!MBCLEN_CHARFOUND(len)) {
    compile_error(PARSER_ARG "invalid multibyte char");
    return -1;
    }
    tokadd(c);
    lex_p += --len;
    if (len > 0) tokcopy(len);
    return c;
}
 
#define parser_precise_mbclen()  rb_enc_precise_mbclen((lex_p-1),lex_pend,parser->enc)

エラー処理はとりあえず無視するとして、以下のことが行われています。

  1. マルチバイト文字のバイト長を取得
  2. マルチバイト文字の1バイト目をトークンに格納
  3. 現在の読み込み位置をマルチバイト文字の後ろに移動
  4. マルチバイト文字の2バイト目以降をトークンに格納

rb_enc_precise_mbclen関数に進みましょう。

encoding.c

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 
-
|
|
|
|
|
|
|
!
rb_enc_precise_mbclen(const char *p, const char *e, rb_encoding *enc)
{
    int n;
    if (e <= p)
        return ONIGENC_CONSTRUCT_MBCLEN_NEEDMORE(1);
    n = ONIGENC_PRECISE_MBC_ENC_LEN(enc, (UChar*)p, (UChar*)e);
    if (e-p < n)
        return ONIGENC_CONSTRUCT_MBCLEN_NEEDMORE(n-(e-p));
    return n;
}

include/ruby/oniguruma.h

Everything is expanded.Everything is shortened.
  1
 
#define ONIGENC_PRECISE_MBC_ENC_LEN(enc,p,e)   (enc)->precise_mbc_enc_len(p,e,enc)

というわけで各エンコーディングに対する処理関数が呼び出され、エンコーディングに応じてバイト列が特定の文字として認識され長さが返されています。

最後にSTR_NEW3マクロを見ていきましょう。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 
 
 
-
|
|
|
|
|
-
|
!
|
|
!
#define STR_NEW3(p,n,e,func) parser_str_new((p),(n),(e),(func))
 
parser_str_new(const char *p, long n, rb_encoding *enc, int func)
{
    VALUE str;
 
    str = rb_enc_str_new(p, n, enc);
    if (!(func & STR_FUNC_REGEXP) &&
        rb_enc_asciicompat(enc) &&
        rb_enc_str_coderange(str) == ENC_CODERANGE_7BIT) {
        rb_enc_associate(str, rb_ascii8bit_encoding());
    }
 
    return str;
}

rb_enc_str_new関数に続く。

string.c

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
 
-
|
|
|
|
!
rb_enc_str_new(const char *ptr, long len, rb_encoding *enc)
{
    VALUE str = str_new(rb_cString, ptr, len);
 
    rb_enc_associate(str, enc);
    return str;
}

encoding.c

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 
-
|
!
 
 
-
|
|
-
|
!
-
|
|
!
|
|
|
!
rb_enc_associate(VALUE obj, rb_encoding *enc)
{
    rb_enc_associate_index(obj, rb_enc_to_index(enc));
}
 
rb_enc_associate_index(VALUE obj, int idx)
{
    enc_check_capable(obj);
    if (!ENC_CODERANGE_ASCIIONLY(obj) ||
        !rb_enc_asciicompat(rb_enc_from_index(idx))) {
        ENC_CODERANGE_CLEAR(obj);
    }
    if (idx < ENCODING_INLINE_MAX) {
        ENCODING_SET(obj, idx);
        return;
    }
    ENCODING_SET(obj, ENCODING_INLINE_MAX);
    rb_ivar_set(obj, rb_id_encoding(), INT2NUM(idx));
    return;
}

include/ruby/encoding.h

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
 
 
 
-
|
|
!
#define ENCODING_INLINE_MAX 1023
#define ENCODING_SHIFT (FL_USHIFT+10)
#define ENCODING_MASK (ENCODING_INLINE_MAX<#define ENCODING_SET(obj,i) do {\
    RBASIC(obj)->flags &= ~ENCODING_MASK;\
    RBASIC(obj)->flags |= i << ENCODING_SHIFT;\
} while (0)

というわけでオブジェクトにエンコーディングのインデックスが設定されています。インデックスはENCODING_INLINE_MAX未満ならflags内に、それ以上ならインスタンス変数に設定されています。1023って多過ぎじゃない?と思うのですが。ruby.hを参照するとFL_USER10〜FL_USER19がエンコーディングのインデックスとして使われるようです。

設定されたエンコーディングの利用

それでは最後にStringの各メソッドでエンコーディングがどう使われているかを見てみましょう。全部見るのはめんどくさいので一番めんどくさそうな"日本語の文字列"[4..-1]だけ見ることにします。String#[]を実装しているのはrb_str_aref関数です。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 
-
|
-
|
|
|
-
|
|
|
|
-
|
|
|
|
|
|
|
!
!
rb_str_aref(VALUE str, VALUE indx)
{
    ...
    switch (TYPE(indx)) {
      ...
      default:
        /* check if indx is Range */
        {
            long beg, len;
            VALUE tmp;
 
            len = str_strlen(str, rb_enc_get(str));
            switch (rb_range_beg_len(indx, &beg, &len, len, 0)) {
              case Qfalse:
                break;
              case Qnil:
                return Qnil;
              default:
                tmp = rb_str_substr(str, beg, len);
                return tmp;
            }
}

str_strlen関数は以下のようになっています。ASCIIのみの文字列の場合は設定されているエンコーディングに処理を投げずにバイト長を返すことで高速化を図っているようです。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 
-
|
|
|
|
|
-
|
!
|
!
 
 
str_strlen(VALUE str, rb_encoding *enc)
{
    long len;
 
    if (is_ascii_string(str)) return RSTRING_LEN(str);
    if (!enc) enc = rb_enc_get(str);
    len = rb_enc_strlen(RSTRING_PTR(str), RSTRING_END(str), enc);
    if (len < 0) {
    rb_raise(rb_eArgError, "invalid mbstring sequence");
    }
    return len;
}
 
#define is_ascii_string(str) (rb_enc_str_coderange(str) == ENC_CODERANGE_7BIT)

rb_enc_str_coderange関数です。少し長めですがカットするところがないのでそのまま載せます。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 
-
|
|
-
|
|
|
|
|
|
-
|
|
|
-
-
|
!
|
!
-
|
|
!
!
|
!
|
!
rb_enc_str_coderange(VALUE str)
{
    int cr = ENC_CODERANGE(str);
 
    if (cr == ENC_CODERANGE_UNKNOWN) {
        rb_encoding *enc = rb_enc_get(str);
 
        const char *p = RSTRING_PTR(str);
        const char *e = p + RSTRING_LEN(str);
 
        cr = rb_enc_asciicompat(enc) ? ENC_CODERANGE_7BIT : ENC_CODERANGE_VALID;
        while (p < e) {
            int ret = rb_enc_precise_mbclen(p, e, enc);
            int len = MBCLEN_CHARFOUND(ret);
 
            if (len) {
                if (len != 1 || !rb_enc_isascii((unsigned char)*p, enc)) {
                    cr = ENC_CODERANGE_VALID;
                }
                p += len;
            }
            else {
                cr = ENC_CODERANGE_BROKEN;
                break;
            }
        }
        ENC_CODERANGE_SET(str, cr);
    }
    return cr;
}

include/ruby/encoding.h

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
 
 
 
 
 
 
#define ENC_CODERANGE_MASK      (FL_USER8|FL_USER9)
#define ENC_CODERANGE_UNKNOWN   0
#define ENC_CODERANGE_7BIT      FL_USER8
#define ENC_CODERANGE_VALID     FL_USER9
#define ENC_CODERANGE_BROKEN    (FL_USER8|FL_USER9)
#define ENC_CODERANGE(obj) (RBASIC(obj)->flags & ENC_CODERANGE_MASK)

というわけでFL_USER8とFL_USER9が使われています。最初はflagsは設定されていないのでifの中身に入り適切な値が設定されます。

続いてrb_enc_strlen関数。同じく文字を表現する最大バイト数と最小バイト数が同じ(例えばISO-8859-1とか)場合は単純に割ることで長さを出すという高速化が行われています。rb_enc_mbclen関数は大雑把に言うと先ほど見たrb_enc_precise_mbclen関数と同じようにエンコーディングの処理関数を呼び出して文字のバイト長を取得しています。

encoding.c

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 
-
|
|
-
|
!
|
-
|
|
|
!
|
!
rb_enc_strlen(const char *p, const char *e, rb_encoding *enc)
{
    long c;
 
    if (rb_enc_mbmaxlen(enc) == rb_enc_mbminlen(enc)) {
    return (e - p) / rb_enc_mbminlen(enc);
    }
 
    for (c=0; p
    int n = rb_enc_mbclen(p, e, enc);
 
    p += n;
    }
    return c;
}

rb_range_beg_len関数をかけることで"日本語の文字列"[4..-1]に対してbeg=>4、len=>3を取得しています。

rb_str_substr関数は引数に応じていろいろ処理が分岐していますが今回のケースで通るところは以下の部分です。else ifは条件を満たさないので中には入りませんがこっそりpが設定されているので要注意です。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 
-
|
|
|
|
|
-
|
-
|
!
|
|
|
|
|
|
!
rb_str_substr(VALUE str, long beg, long len)
{
    rb_encoding *enc = rb_enc_get(str);
    VALUE str2;
    char *p, *s = RSTRING_PTR(str), *e = s + RSTRING_LEN(str);
    int asc = IS_7BIT(str);
    ...
    else if ((p = str_nth(s, e, beg, enc, asc)) == e) {
    ...
    else {
        len = str_offset(p, e, len, enc, asc);
    }
  sub:
    str2 = rb_str_new5(str, p, len);
    rb_enc_copy(str2, str);
    OBJ_INFECT(str2, str);
 
    return str2;
}

str_nth関数およびstr_nth関数が呼び出すrb_enc_nth関数では例によって必要な場合のみエンコーディングの処理関数を呼び出すようになっています。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 
-
|
|
|
|
|
|
|
!
str_nth(char *p, char *e, int nth, rb_encoding *enc, int asc)
{
    if (asc)
        p += nth;
    else
        p = rb_enc_nth(p, e, nth, enc);
    if (!p) return 0;
    if (p > e) return e;
    return p;
}

encoding.c

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 
-
|
|
-
|
!
-
|
!
-
-
|
|
|
!
!
|
!
rb_enc_nth(const char *p, const char *e, int nth, rb_encoding *enc)
{
    int c;
 
    if (rb_enc_mbmaxlen(enc) == 1) {
        p += nth;
    }
    else if (rb_enc_mbmaxlen(enc) == rb_enc_mbminlen(enc)) {
        p += nth * rb_enc_mbmaxlen(enc);
    }
    else {
        for (c = 0; p < e && nth--; c++) {
            int n = rb_enc_mbclen(p, e, enc);
 
            p += n;
        }
    }
    return (char*)p;
}

str_offset関数をかけることで文字列長からバイト長に変換されています。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
 
-
|
|
|
!
str_offset(char *p, char *e, int nth, rb_encoding *enc, int asc)
{
    const char *pp = str_nth(p, e, nth, enc, asc);
    if (!pp) return e - p;
    return pp - p;
}

最後に設定したポインタとバイト長を引数にrb_str_new5関数を呼ぶことで切り出し完了です。

おわりに

今回はRuby1.9でのm17n対応を見てきました。わかったこととして以下があります。

  • エンコーディングの処理は構造体に処理関数を設定し呼び出すことで行われている
  • オブジェクトにはエンコーディング構造体のインデックスが設定されている
  • 必要な場合のみエンコーディングの処理関数を呼び出すようにしている

それではみなさんもよいコードリーディングを。


トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2008-02-10 (日) 00:21:02 (4326d)