はじめに †
次のスクリプトがあり、文字コードは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
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
1
2
|
| #define ONIG_ENCODING_EUC_JP (&OnigEncodingEUC_JP)
ONIG_EXTERN OnigEncodingType OnigEncodingEUC_JP;
|
encoding.h
1
|
| typedef OnigEncodingType rb_encoding;
|
enc/euc_jp.c
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",
3,
1,
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
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
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
1
2
3
4
|
-
|
|
| yycompile0(VALUE arg, int tracing)
{
parser->enc = rb_enc_get(lex_input);
...
|
これでスクリプトを読み込むときにどの文字コードを使うかが設定されました。
続いてスクリプトに埋め込まれているエンコーディング指定を処理する部分を見てみましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
-
|
-
|
-
|
-
-
|
!
!
!
| parser_yylex(struct parser_params *parser)
{
...
switch (c = nextc()) {
case '#':
if (!parser->has_shebang || parser->line_count != 1) {
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関数で行われます。注目部分だけ抜き出すと以下の通り。
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の話をしているかについてはスクリプト解析を読むを参照してください。
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関数です。
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バイト目以降をトークンに格納
rb_enc_precise_mbclen関数に進みましょう。
encoding.c
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
1
|
| #define ONIGENC_PRECISE_MBC_ENC_LEN(enc,p,e) (enc)->precise_mbc_enc_len(p,e,enc)
|
というわけで各エンコーディングに対する処理関数が呼び出され、エンコーディングに応じてバイト列が特定の文字として認識され長さが返されています。
最後にSTR_NEW3マクロを見ていきましょう。
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
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
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
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関数です。
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:
{
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のみの文字列の場合は設定されているエンコーディングに処理を投げずにバイト長を返すことで高速化を図っているようです。
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関数です。少し長めですがカットするところがないのでそのまま載せます。
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
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
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が設定されているので要注意です。
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関数では例によって必要な場合のみエンコーディングの処理関数を呼び出すようになっています。
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
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
int n = rb_enc_mbclen(p, e, enc);
p += n;
}
}
return (char*)p;
}
|
str_offset関数をかけることで文字列長からバイト長に変換されています。
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対応を見てきました。わかったこととして以下があります。
- エンコーディングの処理は構造体に処理関数を設定し呼び出すことで行われている
- オブジェクトにはエンコーディング構造体のインデックスが設定されている
- 必要な場合のみエンコーディングの処理関数を呼び出すようにしている
それではみなさんもよいコードリーディングを。