PHP - mb_internal_encoding()とini_get()の結果が異なるのでPHPのソースを読んで理解してみた

ini_get('mbstring.internal_encoding')mb_internal_encoding() の値が異なるらしいので調べてみました。
後半ではmbstring.languagemb_regex_encoding との影響についても記述します。

ソースを読んで自分なりに解釈しましたが、間違い等あればご指摘ください。

環境

現象を確認

まずは次のコードにて確認してみます。

<?php
// 出力確認用の関数
function p($title) {
    echo $title . PHP_EOL;
    echo '  mb_internal_encoding():' . mb_internal_encoding() . PHP_EOL;
    echo '                 ini_get:' . ini_get('mbstring.internal_encoding') . PHP_EOL;
    echo PHP_EOL;
}

// EUC-JPを初期値とする
function init() {
    ini_set('mbstring.internal_encoding', 'EUC-JP');
    mb_internal_encoding('EUC-JP');
}

init();
p('初期状態');

mb_internal_encoding('UTF-8');
p('mb_internal_encodingで文字コードを指定');

init();
p('ふたたび初期状態');

ini_set('mbstring.internal_encoding', 'UTF-8');
p('ini_setで文字コードを指定');

実行結果

初期状態
  mb_internal_encoding():EUC-JP
                 ini_get:EUC-JP

mb_internal_encodingで文字コードを指定
  mb_internal_encoding():UTF-8
                 ini_get:EUC-JP

ふたたび初期状態
  mb_internal_encoding():EUC-JP
                 ini_get:EUC-JP

ini_setで文字コードを指定
  mb_internal_encoding():UTF-8
                 ini_get:UTF-8

mb_internal_encodingで設定した場合はini_getはそのままでしたが、
ini_setで指定するとどちらも変更されました。

PHPのソースを落としてくる

PHPのソース を落としてきて実装を調べてみます。
今回は特にtag指定せずmasterのまま見ました。

ini_getとmb_internal_encodingの参照値が何なのか調べる

ini_getが返す値を調べる

該当ファイルはbasic_functions.cです。

// ext/standard/basic_functions.c
PHP_FUNCTION(ini_get)
{
    char *varname, *str;
    int varname_len;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &varname, &varname_len) == FAILURE) {
        return;
    }

    str = zend_ini_string(varname, varname_len + 1, 0);

    if (!str) {
        RETURN_FALSE;
    }

    RETURN_STRING(str, 1);
}

PHP_FUNCTIONはマクロで、展開されるといろいろシグネチャを変更した関数定義になります。
これ以降、似たように関数定義で出てくるマクロについては基本説明を省略します。

関数の中身について、ここでは主に次のことをしています。

  1. zend_parse_parametersで引数を展開
    1. varnameがini_get()で指定する引数名
  2. zend_ini_stringで値を取得
  3. 値があればそれを返す

zend_ini_stringが何を返すかが関心の対象です。
定義はzend_ini.cです。

// Zend/zend_ini.c
ZEND_API char *zend_ini_string_ex(char *name, uint name_length, int orig, zend_bool *exists)
{
    zend_ini_entry *ini_entry;
    TSRMLS_FETCH();

    if (zend_hash_find(EG(ini_directives), name, name_length, (void **) &ini_entry) == SUCCESS) {
        if (exists) {
            *exists = 1;
        }

        if (orig && ini_entry->modified) {
            return ini_entry->orig_value;
        } else {
            return ini_entry->value;
        }
    } else {
        if (exists) {
            *exists = 0;
        }
        return NULL;
    }
}

zend_ini_stringは内部でzend_ini_string_exを呼んでいて、処理の本体はこちらなのでexの方を載せました。

ここでは主に次のことをしています。

  1. キーに該当するデータがあるかハッシュテーブルから検索する
    1. データがなければNULL
  2. データの「変更されたフラグ」が立っていれば変更前の値を、そうでなければ現在の値を返す
    • この関数はini_getだけから呼ばれるわけではないのでこういう制御が入っている

つまり、「ini_getはハッシュテーブルにあるデータから値を取得している」と言えます。

mb_internal_encoding()が返す値を調べる

続いてmb_internal_encoding()についても調べます。
定義はmbstring.cです。

// ext/mbstring/mbstring.c
PHP_FUNCTION(mb_internal_encoding)
{
    const char *name = NULL;
    int name_len;
    const mbfl_encoding *encoding;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "|s", &name, &name_len) == FAILURE) {
        RETURN_FALSE;
    }
    if (name == NULL) {
        name = MBSTRG(current_internal_encoding) ? MBSTRG(current_internal_encoding)->name: NULL;
        if (name != NULL) {
            RETURN_STRING(name, 1);
        } else {
            RETURN_FALSE;
        }
    } else {
        encoding = mbfl_name2encoding(name);
        if (!encoding) {
            php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unknown encoding \"%s\"", name);
            RETURN_FALSE;
        } else {
            MBSTRG(current_internal_encoding) = encoding;
            RETURN_TRUE;
        }
    }
}

mb_internal_encoding()は引数のありなしで設定(set)なのか取得(get)なのかを振り分けるので、実装もそのようになっています。

  1. zend_parse_parametersで引数を展開
  2. if (name == NULL)。引数があるかないかで処理を振り分けます
    • いまは取得について調べているので、このif文に入ります
    • else側はまだ見ません
  3. MBSTRG(current_internal_encoding) を通して「現在の内部エンコーディング」を取得します
    • MBSTRG(current_internal_encoding)はマクロ展開の結果、mbstring_globals->current_internal_encoding という形になります
    • アロー演算子はポインタなので、要は「ヌルポインタじゃないならポインタ先の値を参照する」ということです
  4. 内部エンコーディングが存在すればその値を返す

つまり、「mb_internal_encoding()はmbstring_globalsというグローバル変数から値を取得している」と言えます。

ini_getのハッシュテーブルに格納されていたデータがmbstring_globalsという可能性は?

確認するためにも、今度は設定時の挙動の違いを見てみます。

ini_setとmb_internal_encodingの設定先が何なのか調べる

ini_setの参照先を調べる

定義はini_getと同じbasic_functions.cです。
少し長いので引数展開など一部省略します。

// ext/standard/basic_functions.c
PHP_FUNCTION(ini_set)
{
    (省略)

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss", &varname, &varname_len, &new_value, &new_value_len) == FAILURE) {
        return;
    }

    (省略)

    if (zend_alter_ini_entry_ex(varname, varname_len + 1, new_value, new_value_len, PHP_INI_USER, PHP_INI_STAGE_RUNTIME, 0 TSRMLS_CC) == FAILURE) {
        zval_dtor(return_value);
        RETURN_FALSE;
    }
}
  1. zend_parse_parametersで引数を展開
  2. zend_alter_ini_entry_exで設定を更新

zend_alter_ini_entry_exの定義を見てみましょう。
ファイルはzend_ini.cです。
これまた長めなので一部省略します。

// Zend/zend_ini.c
ZEND_API int zend_alter_ini_entry_ex(char *name, uint name_length, char *new_value, uint new_value_length, int modify_type, int stage, int force_change TSRMLS_DC)
{
    (省略)

    if (zend_hash_find(EG(ini_directives), name, name_length, (void **) &ini_entry) == FAILURE) {
        return FAILURE;
    }

    (省略)

    duplicate = estrndup(new_value, new_value_length);

    if (!ini_entry->on_modify
        || ini_entry->on_modify(ini_entry, duplicate, new_value_length, ini_entry->mh_arg1, ini_entry->mh_arg2, ini_entry->mh_arg3, stage TSRMLS_CC) == SUCCESS) {
        if (modified && ini_entry->orig_value != ini_entry->value) { /* we already changed the value, free the changed value */
            efree(ini_entry->value);
        }
        ini_entry->value = duplicate;
        ini_entry->value_length = new_value_length;
    } else {
        efree(duplicate);
        return FAILURE;
    }

    return SUCCESS;
}
  1. zend_hash_find()でハッシュテーブルから該当データを取得します
  2. estrndup()で新しいデータのメモリ確保などして、戻り値をduplicateという変数に保存します
  3. if(...) で更新チェックやイベントを発火して、問題がなければデータを更新します

普通にハッシュテーブルからデータを取ってきているだけのように見えます。

ini_xxxが参照する値はそもそもどんなものなのか

これは結論から言って、PHP_INI_ENTRYというマクロを経由して作られる構造体です。
PHP4のドキュメントで古いんですが一応資料があります。
ini ファイルのサポート

この定義はmbstring.cに書かれています。

// ext/mbstring/mbstring.c
PHP_INI_BEGIN()
    PHP_INI_ENTRY("mbstring.language", "neutral", PHP_INI_ALL, OnUpdate_mbstring_language)
    PHP_INI_ENTRY("mbstring.detect_order", NULL, PHP_INI_ALL, OnUpdate_mbstring_detect_order)
    PHP_INI_ENTRY("mbstring.http_input", "pass", PHP_INI_ALL, OnUpdate_mbstring_http_input)
    PHP_INI_ENTRY("mbstring.http_output", "pass", PHP_INI_ALL, OnUpdate_mbstring_http_output)
    STD_PHP_INI_ENTRY("mbstring.internal_encoding", NULL, PHP_INI_ALL, OnUpdate_mbstring_internal_encoding, internal_encoding_name, zend_mbstring_globals, mbstring_globals)

mbstring.internal_encodingについてはSTD_PHP_INI_ENTRYとなっていますが、きっと似たようなものでしょう。
(ちゃんと中身を見ていない)

ということで、「ハッシュテーブルにあるのはPHP_INI_ENTRYという汎用的なデータ構造」です。

mb_internal_encodingの参照先を調べる

これは見なくても想像つきますので駆け足で。 定義はさきほど載せました。if-elseのelse文の方です。
該当行だけ抜粋します。

MBSTRG(current_internal_encoding) = encoding;

これはつまり、mbstring_globals.current_internal_encoding = encodingということですね。

ここまでのまとめ

  • ini_setで更新されるのはPHP_INI_ENTRY
  • mb_internal_encodingで更新されるのはmbstring_globals
  • 両者の変更先が異なるので、実行結果が異なる

mb_internal_encodingで変更した場合にini側に影響がなかった理由は分かりました。
しかし、ini_setでmb_internal_encodingの値まで変わったのはなぜでしょう。

php.iniの記述を読み込む際やini_setを実行すると更新イベントが走る

処理の流れ

この更新イベントで登録されているハンドラにて、mb_internal_encodingの参照先も更新されているためです。

まず先ほどのPHP_INI_ENTRY(STD_PHP_INI_ENTRY)の定義をもう一度見てみます。

    STD_PHP_INI_ENTRY("mbstring.internal_encoding", NULL, PHP_INI_ALL, OnUpdate_mbstring_internal_encoding, internal_encoding_name, zend_mbstring_globals, mbstring_globals)

この4つの目の引数が、更新時のイベントハンドラになります。
OnUpdate_mbstring_internal_encoding という関数は同じmbstring.cに定義されています。
内部での主な処理は同ファイルにある_php_mb_ini_mbstring_internal_encoding_setという関数に委譲していますので、そちらの定義を見てみます。

長いので一部省略します。

// ext/mbstring/mbstring.c
int _php_mb_ini_mbstring_internal_encoding_set(const char *new_value, uint new_value_length TSRMLS_DC)
{
    (省略)
    MBSTRG(internal_encoding) = encoding;
    MBSTRG(current_internal_encoding) = encoding;
    (省略)
    return SUCCESS;
}

新しい値をcurrent_internal_encodingに設定しています。

つまり、

  1. ini_setを呼ぶ
  2. 更新イベントが走る
  3. (最終的に)_php_mb_ini_mbstring_internal_encoding_setが呼ばれる
  4. (mb_internal_encodingの時と同じ)current_internal_encodingに新しい値が入る

という流れです。

更新イベントの部分

いちおう更新イベント部分の説明をします。
さきほどのSTD_PHP_INI_ENTRYがマクロ展開されたあと、
4つ目の引数(イベントハンドラ)はon_modifyという変数名で定義されます。

そして先程のzend_alter_ini_entry_ex関数を見ます。

// Zend/zend_ini.c
    if (!ini_entry->on_modify
        || ini_entry->on_modify(ini_entry, duplicate, new_value_length, ini_entry->mh_arg1, ini_entry->mh_arg2, ini_entry->mh_arg3, stage TSRMLS_CC) == SUCCESS) {

ここで、ini_entry->on_modify()を実行していますね。

ここまでのまとめ

  • ini_setで更新されるのはPHP_INI_ENTRY
    • 更新時にイベントハンドラが実行される
      • mbstring.internal_encodingの定義で登録されているハンドラが走る
      • 結果的にmbstring_globalsの値も更新される
  • mb_internal_encodingで更新されるのはmbstring_globals
  • 両者の変更先が異なるので、実行結果が異なる

補足

今回の範囲においては、mbstring.languageやmb_regex_encodingも影響するので書きます。

mb_internal_encodingの戻り値はmbstring.languageに影響される

サンプルコードはこちらです。

<?php
// 初期値の確認
echo ini_get('mbstring.language') . PHP_EOL; // neutral
echo ini_get('mbstring.internal_encoding') . PHP_EOL; // 空文字
echo mb_internal_encoding() . PHP_EOL; // ISO-8859-1

echo '-----' . PHP_EOL;
// mbstring.language を設定
ini_set('mbstring.language', 'Japanese');
echo ini_get('mbstring.language') . PHP_EOL; // Japanese
echo ini_get('mbstring.internal_encoding') . PHP_EOL; // 空文字
echo mb_internal_encoding() . PHP_EOL; // ISO-8859-1

echo '-----' . PHP_EOL;
// mbstring.internal_encoding を設定
ini_set('mbstring.internal_encoding', 'hoge');
echo ini_get('mbstring.language') . PHP_EOL; // Japanese
echo ini_get('mbstring.internal_encoding') . PHP_EOL; // hoge
echo mb_internal_encoding() . PHP_EOL; // EUC-JP

大事なところは最後です。

  • mbstring.languageが設定されている状態で
  • ini_set('mbstring.internal_encoding', 'hoge'); をすると
  • ini_get('mbstring.internal_encoding') はhogeを返すのに
  • mb_internal_encoding() はEUC-JPになった!
    • それまでISO-8859-1だったのに
    • (不正な文字コードだけど) hogeでもない

これと同じ原因で、mbstring.languageの説明ではよく以下のことが書かれています。

mbstring.internal_encoding は、 mbstring.language の後に置く必要があることに注意してください
mbstring.language

なぜなのか。コードを読んでみましょう。

さきほどの_php_mb_ini_mbstring_internal_encoding_setで、省略した部分に答えがあります。

// ext/mbstring/mbstring.c
int _php_mb_ini_mbstring_internal_encoding_set(const char *new_value, uint new_value_length TSRMLS_DC)
{
    const mbfl_encoding *encoding;

    if (!new_value || new_value_length == 0 || !(encoding = mbfl_name2encoding(new_value))) {
          switch (MBSTRG(language)) {
              case mbfl_no_language_uni:
                  encoding = mbfl_no2encoding(mbfl_no_encoding_utf8);
                  break;
              case mbfl_no_language_japanese:
                  encoding = mbfl_no2encoding(mbfl_no_encoding_euc_jp);
                  break;
             (省略)
              default:
                  encoding = mbfl_no2encoding(mbfl_no_encoding_8859_1);
                  break;
          }
      }
    MBSTRG(internal_encoding) = encoding;
    MBSTRG(current_internal_encoding) = encoding;
    (省略)
    return SUCCESS;
}
  1. ifにて以下のいずれかに該当すると中に入る
    • 新しい値がヌルポやfalseなどの場合
    • 新しい値の長さが0の場合(空文字など)
    • 新しい値に対応するエンコーディングを取得しようとしたが失敗した
      • hogeなど不正な文字列の場合
  2. languageの定義値に沿って代わりのエンコーディングを決める
    • 該当するものがなければISO-8859-1になる
  3. current_internal_encodingに設定する

ということです。
languageを参照しているから、php.iniなどで「languageの設定を先に書け」と言われるのですね。
不正なエンコーディング文字列を指定しなければいいだけかもしれませんが、
個人的にはなかなか驚きです。

mb_regex_encodingへの影響

サンプルコードはこちらです。

<?php
// 初期値の確認
echo ini_get('mbstring.internal_encoding') . PHP_EOL; // 空文字
echo mb_internal_encoding() . PHP_EOL; // ISO-8859-1
echo mb_regex_encoding() . PHP_EOL; // EUC-JP

echo '-----' . PHP_EOL;
// mb_internal_encodingで設定
mb_internal_encoding('UTF-8') . PHP_EOL;
echo ini_get('mbstring.internal_encoding') . PHP_EOL; // 空文字
echo mb_internal_encoding() . PHP_EOL; // UTF-8
echo mb_regex_encoding() . PHP_EOL; // 空文字

echo '-----' . PHP_EOL;
// ini_setでmbstring.internal_encoding を設定
ini_set('mbstring.internal_encoding', 'SJIS');
echo ini_get('mbstring.internal_encoding') . PHP_EOL; // SJIS
echo mb_internal_encoding() . PHP_EOL; // SJIS
echo mb_regex_encoding() . PHP_EOL; // SJIS

要点は2つです。

  • mb_internal_encodingで指定してもmb_regex_encodingは変わらない
  • ini_setでmbstring.internal_encodingを指定したらmb_regex_encodingも変わった

理由の一つは、これまでと同じくイベントハンドラに処理があるから。
該当処理の定義は、やはり_php_mb_ini_mbstring_internal_encoding_setです。

// ext/mbstring/mbstring.c
int _php_mb_ini_mbstring_internal_encoding_set(const char *new_value, uint new_value_length TSRMLS_DC)
{
    (省略)
#if HAVE_MBREGEX
    {
        const char *enc_name = new_value;
        if (FAILURE == php_mb_regex_set_default_mbctype(enc_name TSRMLS_CC)) {
            /* falls back to EUC-JP if an unknown encoding name is given */
            enc_name = "EUC-JP";
            php_mb_regex_set_default_mbctype(enc_name TSRMLS_CC);
        }
        php_mb_regex_set_mbctype(new_value TSRMLS_CC);
    }
#endif
    return SUCCESS;
}

設定します!

UTF-8の記述とか

あとはini_setとmb_internal_encodingとの細かい違いとして、
指定文字列をそのまま持つか表記を変えて保持するかというのがあります。

<?php
mb_internal_encoding('utf8') . PHP_EOL;
echo mb_internal_encoding() . PHP_EOL; // UTF-8
echo '-----' . PHP_EOL;
ini_set('mbstring.internal_encoding', 'utf8');
echo ini_get('mbstring.internal_encoding') . PHP_EOL; // utf8

mb_internal_encodingは'utf8'が'UTF-8'にちゃんと変わっています。

まとめ

  • ini_setで更新されるのはPHP_INI_ENTRY
    • 更新時にイベントハンドラが実行される
      • mbstring.internal_encodingの定義で登録されているハンドラが走る
      • 結果的にmbstring_globalsの値も更新される
        • 指定値とmbstring.languageの値によって値が変わる
      • さらにmb_regex_encodingも更新される
  • mb_internal_encodingで更新されるのはmbstring_globals
  • 両者の変更先が異なるので、実行結果が異なる

最後に

今回は興味本位で調べましたが、実際の業務ではそもそもmb_internal_encodingとかに頼るべきではありません。

こちらのサイトに非常に分かりやすく書かれていますが、文字コードは明示的に変換するようにしましょう。
PHPの文字化けを本気で解決する