脳みそスワップアウト

揮発性なもので。おもにPHPのこととか。

php5 + JSON-C では JSON_BIGINT_AS_STRING が効かない

phpには、JSONを扱うためのjson拡張が同梱されている。(ext-json(JSON_parser))

しかしこのソース中の以下の一文が問題で、各種Linuxディストリで配布できないことになっている。

ext/json/JSON_parser.c

The Software shall be used for Good, not Evil.

Bug #63520 JSON extension includes a problematic license statement

よって、パッケージでインストールする場合、json拡張はphp同梱のではなく、別のものが使われている。
たとえば Remi の場合は たぶん pecl-jsonc

PHPコードからのインタフェイスはそのままに、実装の下位ライブラリだけ別物というイメージ。
しかし実装が違うため、挙動が微妙に違う。

開発環境がソースからビルドのPHPSTG/本番環境が Remi のPHP というプロジェクトで問題が起きたのでメモ。

JSON-C では JSON_BIGINT_AS_STRING が効かない

私が使っていて問題になったのは、 json_decode()JSON_BIGINT_AS_STRING が効かないというもの。

JSON含まれる大きな数値が、PHP_INT_MAX を超えている場合に文字列として扱うオプション。
JSONの相互運用性を考慮して、基本的には常に付けるべきものだろう。

これが JSON-C だと、この定数は存在するものの指定しても効果がなく、
Noticeを吐きつつ PHP_INT_MAX になる模様。(それ以上の細かな挙動は調べていない)

php-5.5.27(ソースからビルド) + 同梱の ext-json(JSON_parser) の例

PHP_INT_MAX を超えた場合は string となっている。期待通り。

/usr/local/php-5.5.27/bin/php -v
PHP 5.5.27 (cli) (built: Aug 24 2015 13:58:57)
Copyright (c) 1997-2015 The PHP Group
Zend Engine v2.5.0, Copyright (c) 1998-2015 Zend Technologies
    with Xdebug v2.2.5, Copyright (c) 2002-2014, by Derick Rethans

$ /usr/local/php-5.5.27/bin/php -i | grep -i json
json
json support => enabled
json version => 1.2.1


### PHP_INT_MAX -1
$ /usr/local/php-5.5.27/bin/php -r 'var_dump(json_decode("{\"num\":9223372036854775806}", true, 512, 2));'
array(1) {
  'num' =>
  int(9223372036854775806)
}

### PHP_INT_MAX
$ /usr/local/php-5.5.27/bin/php -r 'var_dump(json_decode("{\"num\":9223372036854775807}", true, 512, 2));'
array(1) {
  'num' =>
  int(9223372036854775807)
}

### PHP_INT_MAX +1
$ /usr/local/php-5.5.27/bin/php -r 'var_dump(json_decode("{\"num\":9223372036854775808}", true, 512, 2));'
array(1) {
  'num' =>
  string(19) "9223372036854775808"
}

php-5.5.27(remi) + php-pecl-jsonc-devel-1.3.5-1.el6.remi.5.5.x86_64 の例

PHP_INT_MAX を超えた場合は PHP_INT_MAX に 切り詰め されている。
Notice は >= PHP_INT_MAX から発生。

プロジェクト全体で E_NOTICE を無視するようなコーディングをしていると、
値を間違って解釈してしまったことに気づきすらしない。

各ディストリが勝手にやってることとはいえ、phpのマニュアル通りに動作しなくなるのはどうなのか。

$ php -v
PHP 5.5.27 (cli) (built: Jul 13 2015 07:24:17)
Copyright (c) 1997-2015 The PHP Group
Zend Engine v2.5.0, Copyright (c) 1998-2015 Zend Technologies
    with Xdebug v2.2.5, Copyright (c) 2002-2014, by Derick Rethans

$ php -i | grep -i json
/etc/php.d/json.ini,
json
json support => enabled
json version => 1.3.5
JSON-C version (bundled) => 0.11


### PHP_INT_MAX -1
$ php -r 'var_dump(json_decode("{\"num\":9223372036854775806}", true, 512, 2));'
array(1) {
  'num' =>
  int(9223372036854775806)
}

### PHP_INT_MAX
$ php -r 'var_dump(json_decode("{\"num\":9223372036854775807}", true, 512, 2));'
PHP Notice:  json_decode(): integer overflow detected in Command line code on line 1
PHP Stack trace:
PHP   1. {main}() Command line code:0
PHP   2. json_decode() Command line code:1
array(1) {
  'num' =>
  int(9223372036854775807)
}

### PHP_INT_MAX +1
$ php -r 'var_dump(json_decode("{\"num\":9223372036854775808}", true, 512, 2));'
PHP Notice:  json_decode(): integer overflow detected in Command line code on line 1
PHP Stack trace:
PHP   1. {main}() Command line code:0
PHP   2. json_decode() Command line code:1
array(1) {
  'num' =>
  int(9223372036854775807)
}

まとめ

php-5.5.27 ext-json php-5.5.27 pecl-jsonc php-7.1.4 ext-json
PHP_INT_MAX - 1 ○数値 ○数値 ○数値
PHP_INT_MAX ○数値 △数値, Warning ○数値
PHP_INT_MAX + 1 ○文字列 ×数値, Warning, PHP_INT_MAXに切り詰め ○文字列

JSONの数値型とプログラム言語のキャストはPHPに限った話ではないので、
大きな値になりうる場合は文字列型でやり取りした方がいい。
今回の例には含まれていないが浮動小数も同様。

JSON-C が使われているかどうかの判断自体は、JSON_C_BUNDLED の存在有無でできそう。
でもプログラム中で分岐させるのではなく、文字列でやり取りした方がいい。

<?php
if(defined('JSON_C_BUNDLED')) {
  // JSON-C
}  else {
  // おそらくphp同梱の JSON_parser
}

php7 ではまた別の実装になっている模様。
PHPのJSONライセンス問題が一応決着
試してないが、php同梱なのでおそらく JSON_BIGINT_AS_STRING は 効くだろう 効いた。