Advent Calendar 8日目 Zend Multibyte

この記事は、 PHP5.4 Advent Calendar 2011 (http://atnd.org/events/22473) 8日目です。
そして同時に do_aki Advent Calendar 2011 (http://atnd.org/events/22834) の 8日目でもあります ;-p


PHP5.4 Advent Calendar 2011 の 7日目は [twitter:@Hiraku] さんによる PHP5.4時代のprivateメソッドテスト手法 #php5_4: Architect Note でした。



結構皆さん実用的なことを書かれているようなのですが、あえて空気を読まず、コアなところを掘り下げてみたいと思います。

テーマは「Zend Multibyte」

もともと 5.3系にも存在していた機能で、5.4 になっても目新しいことは特にありませんが、
5.3.8 のソースコードと 5.4.0RC2 のソースコードを比較していて、興味を持ったので紹介。

さらに、5.4.0 にバージョンアップする際に落とし穴になりそうな点を解説します。

5.3系における Zend Multibyte

5.3系では、 Zend Multibyte を有効にすると、mbstring 拡張で mbstring.script_encoding という設定が使えるようになります。

この、 mbstring.script_encoding によって、PHP スクリプトの文字エンコーディングを指定できます。
また、 mbstring.internal_encoding という設定では、 PHP の内部文字エンコーディングを指定できます。


これらを適切に設定することで、 PHP スクリプトを Shift JIS で記述することが出来るようになる。というのはよく言われていました。


例えば、Code.1 をそのまま実行すると、 syntax error になってしまいます。

これは、いわゆる 5C 問題の一つでして、「」 の2byte 目に 0x5C (バックスラッシュ) が含まれるため、直後に続くbyteがエスケープ文字と見なされたことが原因です。

Code.1 Shift JIS によって書かれたPHPスクリプト
<?php
var_dump(bin2hex(""));


ところが、 Conf.1 の設定をしてやれば、スクリプトの文字エンコーディングを Shift JIS として適切に処理しつつ読み込んでくれるようになるので、正常にパースできます。
また、 内部文字エンコーディングも Shift JIS と指定しているので、 「表」 は Shift JIS (0x95 0x5C) として扱われます。

Conf.1
mbstring.script_encoding   = SJIS
mbstring.internal_encoding = SJIS
Conf.1 による Code.1 の実行結果
string(4) "955c"


Conf.2 の設定であれば、 「表」 が utf8 ( 0xE8 0xA1 0xA8 ) として扱われます。

Conf.2
mbstring.script_encoding   = SJIS
mbstring.internal_encoding = UTF-8
Conf.2 による Code.1 の実行結果
string(6) "e8a1a8"


しかし残念なことに Zend Multibyte は、コンパイル時にしか有効にできません。

現在利用している php 5.3系 で Zend Multibyte が有効になっているかどうかは phpinfo() の Zend Multibyte Support の欄が enable になっているかで確認できます。

5.4.0RC2における Zend Multibyte

5.4.0RC2 では、 Zend Multibyte は標準で利用可能になりました。
これにより、再コンパイルせずとも Conf.3 の設定をするだけで、Zend Multibyte が有効になります。
デフォルトは Off ですので、明示的に設定する必要があります。

Conf.3 Zend Multibyte 有効化設定
zend.multibyte = On

mbstring 拡張が必要になるのは変わりませんが、 mbstring 拡張は php で日本語を扱う環境では大抵の場合利用していると思うので、問題なることはないでしょう。



これでめでたしめでたしという話だと良いのですが、そこは PHP。 そうは問屋が卸しません。

Conf.4 の設定をした状態で、先の Code.1 を実行してみると、見事に syntax error になりやがります。

Conf.4
zend.multibyte = On
mbstring.script_encoding   = SJIS
mbstring.internal_encoding = UTF-8



実は、 5.4.0RC2 では、 mbstring.script_encoding という設定が無くなり、替わりに zend.script_encoding で設定するようになっています。


なので、 Conf.5 のように設定してやる必要があります。

Conf.5
zend.multibyte = On
zend.script_encoding = SJIS
mbstring.internal_encoding = UTF-8


これが落とし穴。

このこと、 https://svn.php.net/repository/php/php-src/tags/php_5_4_0RC2/NEWS なんかを見てもどこにも書いてません。

コミットログにはありますけどね。
http://svn.php.net/viewvc?view=revision&revision=306453

うーむ。


ちなみに、mbstring.internal_encoding の設定が無くても syntax error にはなりません。スクリプトの読み込みは Shift JIS として処理されるので。 ただ、内部文字エンコーディング指定が無いので、マルチバイト文字は全て "?" (0x3F) に変換されちゃいますけど。

まとめ その1

  • Zend Multibyte は、5.4.0 で標準で利用可能。 (使うときは zend.multibyte = On してね)
  • php5.3 系で mbstring.script_encoding を使っていた人は、 5.4 にするときは zend.script_encoding に設定し直そう。
  • 内部文字エンコーディン指定は、変わらず mbstring.internal_encoding で指定すること!


ってか、そもそも Shift JIS でPHP スクリプト書くのとかもうやめるべきよね。





                                                                                                                                              • -

というわけで、普通の人向けの解説はここでおしまい。

内部構造について知りたいという頭のおかしい人はさらに先に進もう。

                                                                                                                                              • -

5.3系での zend_multibyte.c と mbstring.c のいびつな関係

5.3系 では、 Zend Multibyte を有効にするためには、 configure 時に、 --enable-zend-multibyte を指定する必要がありました。
これによって、 #define ZEND_MULTIBYTE 1 され、 ZEND_MULTIBYTE 関連のコードが有効になります。

#ifdef ZEND_MULTIBYTE から #endif に囲まれたコードは、 mbstring 拡張をはじめ、いろんなファイルに散らばっていて、有効にした場合のコードが非常に追いづらくなっています。


しかも、Zend Multibyte 本体は Zend/zend_multibyte.c にあるのですが、この中に文字エンコーディングに関する情報が含まれている (zend_encoding 型の定義がたくさん) にもかかわらず、mbstring は mbstring でその情報とは関係なく変換していたりして、なんなのこれはというくらい複雑に入り組んでいたりします。
正直、処理を追いかけていて頭が痛くなってきました。



静的に追ってみた限りでは、mbstring なしでも、Shift JIS の読み込みは出来そうなんですけど、実際に、--enable-mbstring なしに --enable-zend-multibyte のみでコンパイルすると、どうやっても Shift JIS で書かれたスクリプトは syntax error になってしまいます。

頑張って調べてみましたがよく分かりませんでした。動的解析するには時間が足りませんでした。。。

zend_encoding や zend_encoding_table は何のために存在してるの?!

5.4.0RC2 での zend_multibyte.c の役割

そんな混乱も、5.4.0RC2 のソースコードを見ていると癒されます。


zend_multibyte.c の中身は非常にシンプルなモノとなり、実質、インタフェース + 空実装 のみとなっています。

Zend Multibyte が必要とする処理は zend_multibyte_functions 型の構造体に集約され、そこに設定された関数を呼び出すための関数(Code.2)がインタフェースとして定義、実装されています。


Code.2 multibyte utility functions (zend_multibyte.h より抜粋)

/* multibyte utility functions */
ZEND_API int zend_multibyte_set_functions(const zend_multibyte_functions *functions TSRMLS_DC);
ZEND_API const zend_multibyte_functions *zend_multibyte_get_functions(TSRMLS_D);

ZEND_API const zend_encoding *zend_multibyte_fetch_encoding(const char *name TSRMLS_DC);
ZEND_API const char *zend_multibyte_get_encoding_name(const zend_encoding *encoding);
ZEND_API int zend_multibyte_check_lexer_compatibility(const zend_encoding *encoding);
ZEND_API const zend_encoding *zend_multibyte_encoding_detector(const unsigned char *string, size_t length, const zend_encoding **list, size_t list_size TSRMLS_DC);
ZEND_API size_t zend_multibyte_encoding_converter(unsigned char **to, size_t *to_length, const unsigned char *from, size_t from_length, const zend_encoding *encoding_to, const zend_encoding *encoding_from TSRMLS_DC);
ZEND_API int zend_multibyte_parse_encoding_list(const char *encoding_list, size_t encoding_list_len, const zend_encoding ***return_list, size_t *return_size, int persistent TSRMLS_DC);

ZEND_API const zend_encoding *zend_multibyte_get_internal_encoding(TSRMLS_D);
ZEND_API const zend_encoding *zend_multibyte_get_script_encoding(TSRMLS_D);
ZEND_API int zend_multibyte_set_script_encoding(const zend_encoding **encoding_list, size_t encoding_list_size TSRMLS_DC);
ZEND_API int zend_multibyte_set_internal_encoding(const zend_encoding *encoding TSRMLS_DC);
ZEND_API int zend_multibyte_set_script_encoding_by_string(const char *new_value, size_t new_value_length TSRMLS_DC);


さらに、 zend_encoding は実装を持たず、構造すらマルチバイトを処理するモジュール (現在は mbstring ) に委譲する形に作り直されています。
これは美しい! わかりやすい! moriyoshi さん++


これにより、 mbstring とは別の マルチバイト文字処理モジュールを追加しやすくなっているのではないかと思います。

事実、5.3系での、 phpinfo における、Zend Multibyte Support が、 disable / enable だったのに対し、5.4.0RC2 では、 disable / provided by mbstring となっていますし。


ただ、ここまで綺麗に分離して、 スクリプト文字エンコーディング指定を zend.script_encoding にしたのだったら、
内部文字エンコーディングzend.internal_encoding にしちゃえば良かったんじゃないのかなとは思いました。

まとめ その2

  • Zend Multibyte 関連のコードは、オモテに現れる違いはあまりないけど、内部で大きく変更されている。
  • 5.3系のZend Multibyte 複雑すぎ
  • mbstring 以外のマルチバイト文字処理モジュールが生まれるかも?

次は、 [twitter:@srea2431] さんです。