てきとうなさいと べぇたばん

PHPのmbstring関数、arrayを突っ込むとfalseが返ってくるのはPHP 5.6まで

PHP 7.3.0 RC6の ext/mbstring/mbstring.c の mb_strlenの実装部分。

最近あった思い込み

PHPでは、標準搭載された関数(ビルトイン関数)にはマニュアルに載ってない挙動をすることがある。何を言ってるのかというと

  • 引数がstringを指定されてるのにarrayやobjectを突っ込む
  • 引数の数が違う

みたいな話。そういうとき、ビルトイン関数は多分NULLが返ってくるということになっている。http://php.net/manual/ja/functions.internal.phpを引用すると

注意: 関数へのパラメータとして関数が想定しているのとは異なるものを渡した場合、 例えば文字列を想定しているところに配列を渡した場合などの場合は関数の返り値は未定義となります。たいていの場合は NULL を返すでしょう。しかしこれはあくまでも規約にすぎず、これに依存することはできません。

ということなので、タイトルの通りにarrayを突っ込むと「多分NULLだが関数によって違う」のである。その例の一つとしてmbstringモジュールの関数がそうで、mb_strlenなどではfalseになっていたのである。

mbstringは長らくfalseだった

大体のmbstringの関数は、stringが要求されているときにarrayを突っ込むとfalseが返ってくる

PHP 5.6で試してみよう。

$ php -r 'var_dump(mb_strlen(array()));'
PHP Warning:  mb_strlen() expects parameter 1 to be string, array given in Command line code on line 1
bool(false)

こんなふうになっていたので、falseが返ってくるのか、程度にしか思ってなかった。

では、PHP 7.0ではどうなのか。

$ php -r 'var_dump(mb_strlen(array()));'
PHP Warning:  mb_strlen() expects parameter 1 to be string, array given in Command line code on line 1
NULL

あれっ、NULLじゃん。全く知らなかった…

ちなみに、他のmbstringの関数もNULLに変わってた。

ソースを読んでみる

php-srcの「引数を解析して、失敗(不正な引数)だったら」というのはだいたい下のコードに当たる。

PHP 5.6.38のコード https://github.com/php/php-src/blob/PHP-5.6.38/ext/mbstring/mbstring.c#L2212

    PHP_FUNCTION(mb_strlen)
    {
       int n;
       mbfl_string string;
       char *enc_name = NULL;
       int enc_name_len;

       mbfl_string_init(&string);

       if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|s", (char **)&string.val, &string.len, &enc_name, &enc_name_len) == FAILURE) {
           RETURN_FALSE;
       }

PHP 7.0.0のコード。https://github.com/php/php-src/blob/PHP-7.0.0/ext/mbstring/mbstring.c#L2220

    PHP_FUNCTION(mb_strlen)
    {
        int n;
        mbfl_string string;
        char *enc_name = NULL;
        size_t enc_name_len;

        mbfl_string_init(&string);

        if (zend_parse_parameters(ZEND_NUM_ARGS(), "s|s", (char **)&string.val, &string.len, &enc_name, &enc_name_len) == FAILURE) {
            return;
        }

両者のうち、zend_parse_parametersのifブロックに注目してほしいのだけど、PHP 5.6.38ではRETURN_FALSEマクロで片付けていたのに、PHP 7.0.0ではreturnとなっている。

RETURN_FALSEはPHPのfalseを返している(return_value構造体にFALSEをセットしている)。そのままreturnする場合には、NULLになる(return_value構造体に何もしないため)。

いつから変わった

該当のコミットは c998bfaf8600bc38dbcd86b84a6469ed06468012 である。コードとしては約4年前、バージョンとしては7.0.0から入った。

PHP 5.6.x から PHP 7.0.x への移行にもこの内容はなかったように見受けられる。

明らかにおかしい引数を渡す場合、mbstringではエラー処理などでfalseを想定しているコードを書いている場合、思わぬ落とし穴にハマることがある。(そんなコードを書く人がいるのかはちょっとわからないけど)

この件でNULLではないmbstring関数

試したバージョンはPHP 7.3.0RC6

リストを書くのが面倒だったので以下のプログラムを記述した。

結果、以下の関数がNULL以外を返すようだ。E_WARNINGは無視していることに注意

function結果
mb_check_encodingtrue
mb_convert_casefalse
mb_detect_orderfalse
mb_ereg_matchfalse
mb_ereg_replace_callbackfalse
mb_ereg_replacefalse
mb_ereg_search_getpos0
mb_ereg_search_getregsfalse
mb_eregfalse
mb_eregi_replacefalse
mb_eregifalse
mb_regex_set_optionsfalse
mb_splitfalse
mb_substitute_characterfalse

※mb_check_encodingがなぜNULLでもfalseでもなくtrueを返しているのかというと、PHP 7.2からmb_check_encodingは配列を受け入れられるようになったためである。つまり、空の配列というものにエンコーディングのチェックをかけても、エンコーディング上問題がないのでtrueになる。結果として、5.6ではfalse、7.0、7.1はNULL、7.2以降はtrueになることになる。

実は知らなかった

ぼく自身も、知らなかったので非常に恥ずかしいと思いながらこのエントリを書いてる。本当なら知ってなきゃいけないのにって思いながら。とはいえ、5.6から7系に移るときに何かしらの情報がないと大変だろうし(こういうのってCIとかで捕まえられそう?)忘備録として残しておきます。