ruby 1.9 の M17N 対応について

はじめに

本文書は、ruby 1.9 に導入された M17N 対応について、調査した結果や調査に関する作業ログである。
調査の目的は、私がメンテナンスしている Ruby の Readline モジュールを最近 (2008 年 10 月 25 日現在 ) 導入された Encode.default_internal に対応するための知識をつけることである。

用語

本文書で使用する用語を定義する。

Ruby スクリプト
プログラミング言語 Ruby で記述したプログラム。また、そのプログラムを含むファイル。
スクリプトエンコーディング
Ruby スクリプトを記述している文字列のエンコーディングUTF-8 の文字列リテラル正規表現を記述している Ruby スクリプトスクリプトエンコーディングは、UTF-8 である必要がある。スクリプトエンコーディングが不正な場合、コンパイルエラーが発生する。
マジックコメント
Ruby スクリプトの 1 行目か 2 行目に記述する特別なコメント。「# coding: euc-jp」というように記述することで、スクリプトエンコーディングEUC-JP に設定できる。
外部エンコーディング
ファイルから読み込みんだ文字列や、ファイルに書き出す文字列のような、Ruby スクリプトの外部とやりとりする文字列のエンコーディングのこと。
内部エンコーディング
外部からの文字列を Ruby スクリプトの内部で扱う前に自動的に文字エンコーディングを変換することもできる。この変換で使用する文字列のエンコーディングのこと。

M17N 対応

そもそも Ruby の M17N 対応とはどういったものだろう。Rubyスクリプトを記述しているユーザに対するメリットと、ruby 1.9 よりも前の Ruby スクリプトへの影響範囲を中心に調査する。

M17N とは、多言語化 (MultilingualizatioN) のことであり、Ruby の M17N 対応とは、Ruby を多言語化することである。

参考 URL: ローカライズ(地域化)とは - IT用語辞典 e-Words

ruby 1.8 では、Rubyインタプリタ全体で EUC-JP、Shift_JISUTF-8 の中からひとつのエンコーディングを選び、文字列をそのエンコーディングだと想定して処理をすることができる。Ruby スクリプトUTF-8 で記述していれば、ruby -Ku や $KCODE = "u" といったようにして実行する。
逆にいえば、ひとつのエンコーディングしか選択できないということだ。

ruby 1.9 では、文字列 ( String オブジェクト) が、エンコーディング情報を持ち、文字単位で扱うことができる。(ruby 1.8 までのバイト列として扱っていた。)

p "高尾".size #=> 2
p "高尾"[0] #=> 高
p "高尾"[1] #=> 尾
p "高尾"[2] #=> nil

また、エンコーディングを変換する必要がある場合、"高尾".encode("euc-jp")というように String オブジェクトの encode メソッドや Encoding::Converter クラスを利用できる。これが便利な場面はあるだろう。

文字列の比較は、比較対象の文字列のエンコーディングが影響する。例えば、String#= は、両者のエンコーディングが等しくバイト列表現が等しい場合にのみ true を返す。

対応しているエンコーディングは、UTF-8EUC-JP、Shift_JIS だけではない。UTF-16BE、UTF-16LE、UTF-32BE、UTF-32LE にも対応している。
(ruby 1.8 でも iconv や NKF により UTF-16BE などから UTF-8EUC-JP にエンコーディングを変換すれば正規表現マッチなどを実現できるのだが、変換処理が重いという話があるようだ。参考情報: [ruby-dev:36537] Re: Encoding.default_internal のためのパッチ)

また、ruby 1.9 では、Ruby スクリプトごとに文字列のエンコーディングを指定できる。
(というか、指定しないと日本語を解析できず、コンパイルエラーになる。)
例えば、以下のような場合を想定する。

このとき、euc_jp_string.rb、utf_8_string.rb、shift_jis_string.rb の先頭に、それぞれ # coding: euc-jp、# coding: utf-8、# coding: shift_jis と記述することで、それぞれのエンコーディングに則った文字列リテラル正規表現の生成式を直接 Ruby スクリプトに記述できる。

--- euc_jp_string.rb ---
# coding: euc-jp
... EUC-JP の文字列を扱う ...

--- utf_8_string.rb ---
# coding: utf-8
... UTF-8 の文字列を扱う ...

--- shift_jis_string.rb ---
# coding: shift_jis
... Shift_JIS の文字列を扱う ...

ruby 1.8以前では、UTF-8 で記述された Ruby スクリプトから require されるファイルにおいて、EUC-JP のファイルを読み込み、正規表現で特定の文字列を検索する場合、Ruby スクリプトUTF-8 で記述し、NKF モジュールなど利用して、文字列を UTF-8 からEUC-JP に変換が必要であった。

# このファイルはUTF-8 で記述してある。
# ARGF は、 EUC-JP で記述されたファイルを想定する。
require "nkf"
def matched?
  if /#{NKF.nkf("-We", "高尾")}/e.match(ARGF.read)
    puts("#{ARGF.path}は「高尾」を含みます。")
  else
    puts("#{ARGF.path}は「高尾」を含みません。")
  end
end

ruby 1.9 では次のように記述できる。

# coding: euc-jp
def matched?
  if /高尾/.match(ARGF.read)
    puts("#{ARGF.path}は「高尾」を含みます。")
  else
    puts("#{ARGF.path}は「高尾」を含みません。")
  end
end

ruby 1.9 の M17N 対応は、Ruby のリファレンスマニュアルが詳しい。http://doc.loveruby.net/refm/api/view/class/String#m17n
(たぶん、 naruse さんが書かれたのだと思われる。非常に詳しいし、何かわからないことがあれば、ruby-list や ruby-dev で質問すれば、naruse さんに回答していいただけそうな感じだ。すばらしい。naruseさんからの指摘でsheepmanさんが書かれたということが分かりました。失礼しました。)

文字列のエンコーディングの指定方法

ruby 1.9 において、文字列のエンコーディングを指定する方法を述べる。

マジックコメント(# coding: ...、# encoding: ...)

Ruby スクリプトの 1 行目か 2 行目に、次のマジックコメントを書く。

# coding: euc-jp

または次のもの。

# encoding: euc-jp

すると、その Ruby スクリプトの文字列リテラル正規表現の生成式のエンコーディング(以下、スクリプトエンコーディング)を指定できる。

コマンドラインオプション-K

コマンドラインオプション -K」は推奨していない。新しくスクリプトを記述するときは-Kを想定したものにしないこと。(2009年2月1日追記)

コマンドラインオプション -K により、スクリプトエンコーディングを指定できる。
指定できるスクリプトエンコーディングを次に示す。

-KE、-Ke
EUC-JP
-KS、-Ks
Windows-31J(いわゆる Shift_JIS)
-KU、-Ku
UTF-8
-KN、-Kn、-KA、-Ka
ASCII-8BIT
-K<エンコーディング名>(例えば -Keuc-jp)
指定したエンコーディング

ただし、-Kx よりもマジックコメントのほうが優先される。例えば、-Ku を指定していても、Ruby スクリプトに 「# coding: euc-jp」 の記述があれば、スクリプトエンコーディングeuc-jp となる。

コマンドラインオプション-E

コマンドラインオプション -E により、ファイルから読み込みんだ文字列や、ファイルに書き出す文字列のような、Ruby スクリプトの外部とやりとりする文字列のデフォルトのエンコーディング(以下、外部エンコーディング、external encoding)を指定できる。
また、外部からの文字列を Ruby スクリプトの内部で扱う前に自動的にエンコーディングを変換することもできる。このときに使用するデフォルトのエンコーディング(以下、内部エンコーディング、internal encoding)もコマンドラインオプション -E で指定できる。

-E<外部エンコーディング名>(例えば、-Eeuc-jp)
デフォルトの外部エンコーディングを指定する。
-E<外部エンコーディング名>:<内部エンコーディング名>(例えば、-Eeuc-jp:utf-8)
デフォルトの外部エンコーディングと、デフォルトの内部エンコーディングを指定する。

なお、ファイルからの読み書き時などに、エンコーディングを明示することもできる。

# UTF-8 での読み込み。
File.open(path, "r:utf-8") do |fr|
  data = fr.read
  # EUC-JP での書き込み。
  File.open(path, "w:euc-jp") do |fw|
    fw.write(data.encode("euc-jp"))
  end
end
String#encode、String#encode!

文字列のエンコーディングを指定するというのとは少し違うかもしれないが、String オブジェクトの encode メソッド(以下、String#encode)や、encode! メソッド(以下、String#encode!) でエンコーディングを変換できる。

# coding: euc-jp

p "私は高尾です".encoding
#=> #<Encoding:EUC-JP>

p "私は高尾です".encode("utf-8").encoding
#=> #<Encoding:UTF-8>

正規表現の場合、次のようにしてエンコーディングを変換できた。正しい方法があるのかもしれない。

# coding: euc-jp

p(/私は高尾です。/.encoding)
#=> #<Encoding:EUC-JP>

p(/#{"私は高尾です。".encode("utf-8")}/.encoding)
#=> #<Encoding:UTF-8>

以下に transcode.c の String#encode と String#encode! の RDoc 示す。

/*
 *  call-seq:
 *     str.encode!(encoding [, options] )   => str
 *     str.encode!(dst_encoding, src_encoding [, options] )   => str
 *
 *  The first form transcodes the contents of <i>str</i> from
 *  str.encoding to +encoding+.
 *  The second form transcodes the contents of <i>str</i> from
 *  src_encoding to dst_encoding.
 *  The options Hash gives details for conversion. See String#encode
 *  for details.
 *  Returns the string even if no changes were made.
 */

/*
 *  call-seq:
 *     str.encode(encoding [, options] )   => str
 *     str.encode(dst_encoding, src_encoding [, options] )   => str
 *     str.encode([options])   => str
 *
 *  The first form returns a copy of <i>str</i> transcoded
 *  to encoding +encoding+.
 *  The second form returns a copy of <i>str</i> transcoded
 *  from src_encoding to dst_encoding.
 *  The options Hash gives details for conversion. Details
 *  to be added.
 *  The last form returns a copy of <i>str</i> transcoded to
 *  <code>Encoding.default_internal</code>.
 */

残念なことだが、現在の RDoc の処理系では上記の RDoc を正しく解析することができない。このため、RDoc を記述してあるにも関わらず、ri コマンドで String#encode や String#encode! のリファレンスを閲覧できない。

なお、string.c を grep しても String#encode の定義が見つからなかったので、少し戸惑った。その後、transcode.c に文字列のエンコーディングの変換処理が記述してあることが分かった。

String#force_encoding

String オブジェクトの force_encoding メソッド(以下、String#force_encoding) は、文字列のエンコーディングを指定できる。String#encode とは違い、エンコーディングの変換は行わない。このため、次のようなことが起こる。

# coding: utf-8
p "私は高尾です。"
#=> "私は高尾です。"
p "私は高尾です。".force_encoding("EUC-JP")
#=> "&#65533;\x81\xE3\x81&#65533;&#65533;\xAB\x98尾&#65533;\x81&#65533;&#65533;\x81\x99\xE3\x80\x82"

私は、String#force_encoding を UTF-8 で記述したファイルから読み込んだ文字列を UTF-8 として認識されるために使用したことがある。
しかし、これはあまりいい例ではない。詳しくはnaruseさんのコメントを参照してください。(2009年2月1日追記)

# coding: utf-8

# テストです。
path = ARGV.shift
File.open(path, "rb") do |f|
  s = f.read
  p s.encoding #=> #<Encoding:ASCII-8BIT>
  s.force_encoding("utf-8")
  p s.encoding #=> #<Encoding:UTF-8>
  puts(s)
end

なお、上記の Ruby スクリプトは、次のように File#open にエンコーディングを指定するほうが良いだろう。

# coding: utf-8

# テストです。
path = ARGV.shift
File.open(path, "r:utf-8") do |f|
  s = f.read
  p s.encoding #=> #<Encoding:UTF-8>
  puts(s)
end
Encoding::Converter

Encoding::Converter クラスを使用することで、String#encode のように文字列のエンコーディングを変換できる。String#encode と違い、UTF-16BE などにも変換できる。String#encodeでもUTF-16BEに変換できるとのこと。詳しくはnaruseさんのコメントを参照してください。(2009年2月1日追記)

# coding: euc-jp

ec = Encoding::Converter.new("euc-jp", "UTF-16BE")
p ec.convert("私は高尾です。").encoding
#=> #<Encoding:UTF-16BE>

p /#{ec.convert("私は高尾です。")}/.encoding
#=> #<Encoding:UTF-16BE>
その他

ruby 1.9 では、$KCODE は廃止された。

$KCODE = "euc-jp" #=> tmp/euc-jp.rb:6: warning: variable $KCODE is no longer effective; ignored

文字列のエンコーディングに関する C API

ようやく本題である、文字列のエンコーディングに関する C API について述べる。
(これで Readline モジュールを修正できる。)
include/ruby/encoding.h で宣言されている全ての API を説明できればいいのだが、そんなことは私にはできない。(そんなに時間はないのだ。)
そこで、次のヘッダファイルで宣言、定義されている関数やマクロで私が気になったものについて述べる。

rb_encoding

まずは、Ruby における文字列のエンコーディングの表現を調べる。

全文検索システムRast を開発したとき、1 文字進める処理や、記号かどうかの判定などの処理を、文字列のエンコーディング毎に用意した。こうすることで、UTF-8EUC-JP の文書を扱えるようにした。
ruby 1.9 でもそれと同様の仕組みがある。エンコーディングごとに rb_encoding 構造体 (OnigEncodingType 構造体を typedef している) があり、構造体のメンバに、文字に関する関数のポインタなどを持っている。
以下、oniguruma.h より。

typedef struct OnigEncodingTypeST {
  int    (*precise_mbc_enc_len)(const OnigUChar* p,const OnigUChar* e, struct OnigEncodingTypeST* enc);
  const char*   name;
  int           max_enc_len;
  int           min_enc_len;
  int    (*is_mbc_newline)(const OnigUChar* p, const OnigUChar* end, struct OnigEncodingTypeST* enc);
  OnigCodePoint (*mbc_to_code)(const OnigUChar* p, const OnigUChar* end, struct OnigEncodingTypeST* enc);
  int    (*code_to_mbclen)(OnigCodePoint code, struct OnigEncodingTypeST* enc);
  int    (*code_to_mbc)(OnigCodePoint code, OnigUChar *buf, struct OnigEncodingTypeST* enc);
  int    (*mbc_case_fold)(OnigCaseFoldType flag, const OnigUChar** pp, const OnigUChar* end, OnigUChar* to, struct OnigEncodingTypeST* enc);
  int    (*apply_all_case_fold)(OnigCaseFoldType flag, OnigApplyAllCaseFoldFunc f, void* arg, struct OnigEncodingTypeST* enc);
  int    (*get_case_fold_codes_by_str)(OnigCaseFoldType flag, const OnigUChar* p, const OnigUChar* end, OnigCaseFoldCodeItem acs[], struct OnigEncodingTypeST* enc);
  int    (*property_name_to_ctype)(struct OnigEncodingTypeST* enc, OnigUChar* p, OnigUChar* end);
  int    (*is_code_ctype)(OnigCodePoint code, OnigCtype ctype, struct OnigEncodingTypeST* enc);
  int    (*get_ctype_code_range)(OnigCtype ctype, OnigCodePoint* sb_out, const OnigCodePoint* ranges[], struct OnigEncodingTypeST* enc);
  OnigUChar* (*left_adjust_char_head)(const OnigUChar* start, const OnigUChar* p, const OnigUChar* end, struct OnigEncodingTypeST* enc);
  int    (*is_allowed_reverse_match)(const OnigUChar* p, const OnigUChar* end, struct OnigEncodingTypeST* enc);
  int ruby_encoding_index;
} OnigEncodingType;

ここで、私にはひとつ疑問があった。OnigEncodingType 構造体は oniguruma.h に定義してある。ruby 1.9 の重要な文字列の処理が、正規表現のライブラリである「鬼車(http://www.geocities.jp/kosako3/oniguruma/index_ja.html)」に依存しているのはなぜだろう?
ということで、まつもとさんに直接聞いてみた。すると、以下のようなことを教えてもらった。(以下、一字一句正確なわけではない。)
「もともとは、ruby の M17N 対応でやっていたコードを参考にして、様々な文字列のエンコーディングに鬼車が対応した。(以前の) 鬼車には、ruby の M17N のコードを使用するようにコンパイルオプション(? #ifdef だろうか?)もあった。ruby 1.9 に鬼車がとりこまれ、その後 M17N 対応が取り込まれたので、自然に OnigEncodingType を使用することになった。」
ふむふむ。

それでは、文字列のエンコーディングを利用することを想定し、必要な API を挙げながら、機能と使用方法を調査する。

VALUE rb_enc_str_new(const char *ptr, long len, rb_encoding *enc);

ptr と len を元に String オブジェクトを生成し、enc で指定したエンコーディングの情報を付加する。
エンコーディングの情報は、String オブジェクトの RBasic 構造体か、またはインスタンス変数に格納している。格納している情報は UTF-8 なら UTF-8 用の rb_encoding 構造体を示す数値である。
オブジェクトにエンコーディングの情報を付加するのは、rb_enc_associate 関数で行う。
なお、インスタンス変数ではあるものの、変数名が "encoding" であるため Ruby スクリプトからアクセスすることはできない。

s = "高尾です。"
s.instance_variable_get("encoding")
# => NameError: `encoding' is not allowed as an instance variable name
#            from (irb):2:in `instance_variable_get'
#            from (irb):2
#            from /Users/kouji/local/bin/irb19trunk:12:in `<main>'
VALUE rb_enc_reg_new(const char *s, long len, rb_encoding *enc, int options)

s と len を元に Regexp オブジェクトを生成し、enc で指定したエンコーディングの情報を付加する。この関数の中で、rb_reg_initialize 関数を呼び出す。rb_reg_initialize 関数で、Regexp オブジェクトにエンコーディングを付加する処理を実現している。

long rb_enc_strlen(const char *p, const char *e, rb_encoding *enc);

エンコーディングの情報を元に、文字列の先頭のポインタ p から 最後のポインタ e までの文字列の長さを取得する。

VALUE rb_obj_encoding(VALUE obj);

Object#encoding の実装。obj で指定したオブジェクトのエンコーディングを取得する。

文字の種類の判定

enc で指定したエンコーディングを考慮した、文字の種類を判定するマクロを以下に示す。

改行
#define rb_enc_is_newline(p,end,enc)
文字集合
#define rb_enc_isctype(c,t,enc)
アスキー文字
#define rb_enc_isascii(c,enc)
アルファベット
#define rb_enc_isalpha(c,enc)
小文字
#define rb_enc_islower(c,enc)
大文字
#define rb_enc_isupper(c,enc)
印刷文字(アルファベット、数字、制御文字、空白文字を含まない)
#define rb_enc_ispunct(c,enc)
アルファベット、または記号
#define rb_enc_isalnum(c,enc)
印刷文字(制御文字以外)
#define rb_enc_isprint(c,enc)
空白
#define rb_enc_isspace(c,enc)
数字
#define rb_enc_isdigit(c,enc)
よく使用する(であろう)エンコーディングの取得
ASCII-8BIT
rb_encoding *rb_ascii8bit_encoding(void);
UTF-8
rb_encoding *rb_utf8_encoding(void);
US-ASCII
rb_encoding *rb_usascii_encoding(void);
ロケールエンコーディング
rb_encoding *rb_locale_encoding(void);
ファイルシステムエンコーディング(※)
rb_encoding *rb_filesystem_encoding(void);
デフォルトの外部エンコーディング
rb_encoding *rb_default_external_encoding(void);
デフォルトの内部エンコーディング
rb_encoding *rb_default_internal_encoding(void);

Windows だとロケールMac だと UTF8-MAC、それ以外はデフォルトの外部エンコーディング

VALUE rb_str_export(VALUE str);

文字列をファイルに書き出すときのように、Ruby スクリプトの外部へ文字列を渡すときに使用し、str で指定した String オブジェクトを複製し、デフォルトの外部エンコーディングに変換した String オブジェクトを生成する。

#define ExportStringValue(v)

rb_check_safe_obj 関数でオブジェクトが汚染されていないことを確認してから、v で指定した String オブジェクトに対して上記の rb_str_export 関数を処理する。

VALUE rb_str_export_locale(VALUE str);

ロケールに依存するライブラリに文字列を渡す場合などに使用し、str で指定した String オブジェクトを複製し、ロケールエンコーディングに変換した String オブジェクトを生成する。

VALUE rb_external_str_new(const char *ptr, long len);

文字列をファイルから読み込む場合などに使用し、文字列へのポインタ ptr とそのバイト数 len を元に、Ruby スクリプトの外部の文字列に対して、一旦デフォルトの外部エンコーディングの情報を付加する。そして、デフォルトの内部エンコーディングに変換した String オブジェクトを生成する。

VALUE rb_locale_str_new(const char *ptr, long len);

Ruby スクリプトの外部の文字列に対して、一旦ロケールエンコーディングの情報を付加する。そして、デフォルトの内部エンコーディングに変換した String オブジェクトを生成する。
前述した rb_external_str_new 関数と同じようなものだが、こちらはロケールに従って処理する GNU Readline などの C のライブラリから取得した文字列を操作するときなどに使用する。

VALUE rb_usascii_str_new(const char *ptr, long len);

US-ASCII のエンコーディングの情報を付加した String オブジェクトを生成する。

実際に使用した例

これは ruby 1.9 に添付されている Readline モジュール を見てください:-)

まとめ

ruby 1.9 では、M17N 対応の成果により、文字列がエンコーディングの情報を含むようになった。それに関する調査により、Readline モジュールを M17N 対応するための知識を十分に得る事ができた。
実は、Readline モジュールはまつもとさんが M17N 対応をされたので、私がやる作業はほんの少ししか残されていない。(ヒストリを GNU Readline ライブラリに渡すところくらいだろう。)
しかしながら、Curses モジュールやまだまだ M17N 対応できていない添付ライブラリも多いのが現状である。
今回、調査した成果を活かし、12/25 のリリースに向けて、M17N 対応のお手伝いができればと思う。