PECL JSON のソースコードを読んでみる: 導入 〜 json_encode, zval, smart_str 編

Msgpack の PECL(The PHP Extension Community Library) 実装*1を作るに当たって、とりあえず PECL な拡張モジュールの実装がどんなものかをざっと見てみようかと思う。本当ならば、拡張モジュール開発者向けの公式の資料などを見るべきなのだろうが、ちょっとググっただけでは日本語資料は無いようであるし、いい機会なので他人のソースコードを眺めてみるとする。

ちなみに、筆者は Zend Engine についてや C 言語についてはあまり経験や知識もない上、Zend API などの仕様書をろくに読まずに*2エントリを書いているため、エントリの内容が勘違いを含んでいる可能性は低くないことに留意してほしい。

内容

  • ソースコードの入手
  • string json_encode(mixed data) の概要
  • json_encode の正体
  • いろいろな中身の zval
  • zval の型
  • zval の値
  • smart_str
  • Zend 関連で出てきた登場人物 と 参考資料 のまとめ

ソースコードの入手

肝心の PECL JSONソースコードは、http://cvs.php.net/viewvc.cgi/pecl/json/ か、

cvs -d:pserver:cvsread@cvs.php.net:/repository login
cvs -d:pserver:cvsread@cvs.php.net:/repository co pecl/json

で入手できる。cvs login 時のパスワードは phpfi だそうだ(参考: http://php.benscom.com/manual/ja/install.pecl.downloads.php)。

string json_encode(mixed data) の概要

参考: http://php.benscom.com/manual/ja/function.json-encode.php

PECL JSON の提供する主要な関数の一つとして、json_encode がある。この関数は、引数に受け取った値を JSONエンコードして、JSON な文字列を返すというものである。json_encode では、数値、文字列、(連想)配列*3、オブジェクトを引数に受け取る。

この json_encode のソースコードを読めば、PHP(Zend Engine) 側の値を Msgpack されたバイト列に落とすための PHP 拡張関数を作る際の参考になりそうである。特に Zend Engine 上での値の読み取り方や、文字列(バイト列)の構築方法を知るために役立ちそうである。

余談だが、json_encode では、オブジェクトは確実に { ... } という形の JSONエンコードされるが、配列の場合は [ ... ] という形式の JSON や { ... } という形式の JSON のどちらにエンコード結果がなるのかは、配列の持っている要素(のキー)に応じて決定される*4点には注意が必要であったりもする。

json_encode の正体

というわけで、落としてきたソースコードを見てみることとする。PECL JSON が Zend Engine に拡張として提供している関数(json_encode など)の実体は json.c に入っているので、これを見てみると以下のようなコードがある。

/* {{{ proto string json_encode(mixed data) U
   Returns the JSON representation of a value */
static PHP_FUNCTION(json_encode)
{
	zval *parameter;
	smart_str buf = {0};
	long options = 0;

	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z|l", &parameter, &options) == FAILURE) {
		return;
	}

	json_encode_r(&buf, parameter, options TSRMLS_CC);

	/*
	 * Return as binary string, since the result is 99% likely to be just
	 * echo'ed out and we want to avoid overhead of double conversion.
	 */
	ZVAL_STRINGL(return_value, buf.c, buf.len, 1);

	smart_str_free(&buf);
}

コメントを見れば明らかなように、この関数*5json_encode の実体なようだ。

また、static PHP_FUNCTION(json_encode) では、Zend(PHP) 側から渡された引数をパースしたり、戻り値を retrun するといった処理の他は全て json_encode_r という関数に丸投げしているようであり、json_encode_r が json_encode の本体なようである。したがって、Zend が扱っているいろいろな値の処理方法を見るためには、json_encode_r を読み解くと良さそうだ。

ちなみに、json_encode 先頭で渡された引数のパースに用いられている zend_parse_parameters 関数は、"z|l" といった引数のフォーマット(?)文字列を元に、Zend(PHP) から渡された引数たちを、C 言語拡張が扱いやすい形にしたり、エラー処理*6をしてくれるものなようだ。PHP から渡された引数を得たい場合、これを活用しない手はなさそうである。

加えて、戻り値の処理にも色々と疑問な点があるが、これは後述する zval の扱い方などを見た後のほうがわかりやすそうなので、あとで見ることにする。

いろいろな中身の zval

して、json_encode_r 関数はというと、

static void json_encode_r(smart_str *buf, zval *val, int options TSRMLS_DC) /* {{{ */
{
	switch (Z_TYPE_P(val))
	{
		case IS_NULL:    // (中略)
		case IS_BOOL:    // (中略)
		case IS_LONG:    // (中略)
		case IS_DOUBLE:  // (中略)
		case IS_STRING:  // (中略)
		case IS_UNICODE: // (中略)
		case IS_ARRAY:   // (中略)
		case IS_OBJECT:  // (中略)
	}

	return;
}

という風に、受け取った zval *val *7の種類ごとにひたすら処理を進めてゆく関数になっている。このことから、zval はいわゆる「タグ付き共用体」などと呼ばれる類のデータ型で、これが Zend Engine の扱うさまざまなデータを任意に格納できる代物であると想像が付く。

と、ここまでエントリを書き進めたところで、ふと zval についてググってみたところ、DSAS開発者の部屋:PHP Extension を作ろう第2回 - 引数と返値 という記事を発見できた。このエントリで具体的な使い方に言及していない zend_parse_parameters なども含めてさらっと一通り言及されており、参考になる内容である。というより、正直 PHP 拡張を作るだけならそちらの記事を読む方が早いであろう。件の記事には

これらの詳細については PHP のドキュメントをご覧下さい。と言いたい所なのですが現状ではこの辺りのドキュメントは十分無く、一番のドキュメントと呼べるのは PHPソースコード、または 既存の PECL モジュールが参考になると思います。

DSAS開発者の部屋:PHP Extension を作ろう第2回 - 引数と返値

とあることであるし、このエントリではタイトル通り、「既存の PECL モジュールを参考に」の課程をメモしてゆくにとどめておくとしよう*8

zval の型

zval が Zend Engine の扱えるいろいろな型を好きに格納できるコンテナである以上、zval の格納している値の型を任意に調べたり、あるいは設定したりといったことがしたくなるものである。具体的には、今回の json_encode では引数に様々な型の値が渡される関係上、渡された zval の型を調べる必要がある。

上の json_encode_r のソースコードを見れば分かるとおり、zval* がどの型の値を保持しているのかを調べるには Z_TYPE_P というマクロ*9を使うと良いようだ。そして、Z_TYPE_P の戻り値は IS_(型名) *10という定数であるようだ。

型に関して、細かい点としては、IS_NULL が存在するのと、IS_STRING と IS_UNICODE という定数がそれぞれ存在する点が上げられるだろう。Zend(PHP) は null も値の種類の一種として扱っているようである。また、STRING と UNICODE を区別するのは、おそらく PHP 6 で バイナリ文字列*11Unicode文字列 という二つを区別するようになる関係であろう。ちなみに、バイナリ文字列は、いかなる文字エンコーディングにも適合しないバイナリであっても問題ない代物であり、たとえば msgpack した結果のバイト列とかでも良いというものである。

zval の値

ここまでで zval の型の取得について見てきたが、こうなると zval の値をどう取り出すかも気になる。というわけで改めて json_encode_r のソースコードの先ほど中略した部分の一部を見てみると、

case IS_LONG:
	smart_str_append_long(buf, Z_LVAL_P(val));
	break;
case IS_STRING:
case IS_UNICODE:
	json_escape_string(buf, Z_UNIVAL_P(val), Z_UNILEN_P(val), Z_TYPE_P(val), options TSRMLS_CC);
	break;

のように、Z_LVAL_P や Z_UNIVAL_P, Z_UNILEN_P といったマクロが目に付く。このことから、zval* から値を取り出すには、Z_なんとかVAL_P というマクロを使えばよく、文字列の長さは Z_UNILEN_P マクロで別途取得できることが分かる。文字列長が別に保持されているのは、文字列の最中に \0 を混ぜることも出来る*12仕様があってのためでもあろう*13

なお、zval 配列要素やオブジェクトメンバについては、json_encode_r から

case IS_ARRAY:
case IS_OBJECT:
	json_encode_array(buf, &val, options TSRMLS_CC);
	break;

のようにして呼び出されている json_encode_array の中身を見れば、これまでと似た要領でおおよそ分かりそうなものである。また、json_encode_r は、_r という接尾辞*14からしても、おそらく json_encode_r -> json_encode_array -> json_encode_r という風に再帰的に呼び出されて使われるのであろう。こうすることで、入れ子になった配列やオブジェクトなどが処理されていると考えられる。今回はとりあえず json_encode のおおざっぱな流れと、zval などの雰囲気を知ることが主なので、この辺はスルーしておき、後日見るとしよう。

smart_str

ここまでで、zval についてざっと見てきたことで、PHP 拡張な関数(json_encode)に渡された値である zval をどう読み取ればよいのかがおおよそ掴めた。json_encode は与えられた zval を JSON 文字列にして返すものであるからして、あとは文字列の構築の流れを見れば json_encode のやっていることがおおよそ分かりそうである。

そして文字列の構築と言えば、json_encode_r(smart_str *buf, ...) がまさにそれをやっており、

case IS_BOOL:
	if (Z_BVAL_P(val)) {
		smart_str_appendl(buf, "true", 4);
	} else {
		smart_str_appendl(buf, "false", 5);
	}
	break;
case IS_LONG:
	smart_str_append_long(buf, Z_LVAL_P(val));
	break;

に見られるように、smart_str_ 系の関数が文字列の末尾に文字列を追加していることが伺える。

ここで smart_str_ 系の関数の操作対象である buf は smart_str* 型であり、どうやらこれが Zend Engine で使える文字列型なようである*15

smart_str* をどうやって確保&解放するかについては、json_encode_r の呼び出し元を見る必要があるので、static PHP_FUNCTION(json_encode) を見てみると、

smart_str buf = {0};

// (中略: 引数の処理)

json_encode_r(&buf, parameter, options TSRMLS_CC);

/*
 * Return as binary string, since the result is 99% likely to be just
 * echo'ed out and we want to avoid overhead of double conversion.
 */
ZVAL_STRINGL(return_value, buf.c, buf.len, 1);

smart_str_free(&buf);

となっており、解放については smart_str_free で可能なようだ。smart_str の取得については = {0} の部分が謎であり、これについては phpize コマンドなどによる C ソースコードの変換などについて調べる必要がありそうである。ただ、名前から推測して "smart_str_alloc" でググってみたところ PECL 関連のソースコードが色々と引っかかった・・・もののよく分からないことに代わりがないので調べてみたところ、smart_str API : RooJSolutions というページが見つかった。

どうやら、smart_str の作法としては

smart_str *sstr;
sstr = emalloc(sizeof(smart_str));
smart_str_sets(*sstr1, strdup("the cat"));
// ... do something ...    
smart_str_free(sstr);

のようにするのが正しいそうだ。また、リンク先によると、smart_str は長さとバイト列へのポインタだけを持つ構造体なので、

  • smart_str それ自体
  • smart_str が指す先のバイト列

の二つをそれぞれ emalloc する必要があるが、

  • smart_str が指す先のバイト列については smart_str_append といった類の関数が自動で確保してくれる
  • smart_str それ自体は使う側で確保する必要がある

のだそうだ。今回の json_encode で = {0} という謎の初期化を行っている部分は、おそらくどこかで emalloc されたものを受け取っている、のだろう。

なお、emalloc というのは、Zend API にある malloc ラッパーで、malloc 失敗時のエラーを自動的に処理してくれるもののようである*16

Zend 関連で出てきた登場人物 と 参考資料 のまとめ

そんなこんなで、おおざっぱだが json_encode の実装や zval, smart_str の概要について知ることが出来た。今後は、これを元に Msgpack の C 言語実装を PECL 化したりしてゆこうかとおもう。また、今回謎であった部分も、随時明らかにしてゆきたいものである。

まとめとしては微妙だが、とりあえず今回言及した Zend API 関連の諸々と参考しさせてもらった資料を並べてみたのが下記である。今後見返すときに役に立つ、と嬉しい。

~おなまえ ~概要
zval Zend Engine が扱えるデータ全般を格納するデータ型
Z_TYPE_P(zval) zval* から型を表す定数を取り出すマクロ
IS_(型名) zval の型を表す定数たち
Z_(型の略称)VAL_P zval* の保持している値を取り出すマクロたち
Z_UNILEN_P zval* の保持している文字列の長さを取り出すマクロ
zend_parse_parameters(...) PHP 拡張な関数に渡された引数を取得するための便利関数
smart_str Zend Engine で使える文字列型
smart_str_append 系 smart_str に対する文字列連結
smart_str_free smart_str の解放
emalloc Zend Engine によるエラー処理付き malloc
zend_error PHP(Zend)エラーを生起するための関数
~ページ ~概要
http://cvs.php.net/viewvc.cgi/pecl/json/ PECL JSON ソースコード
http://php.benscom.com/manual/ja/install.pecl.downloads.php PECL 関連ソースコードの入手方法
DSAS開発者の部屋:PHP Extension を作ろう第2回 - 引数と返値 PHP 拡張の作り方のチュートリアル
smart_str API : RooJSolutions smart_str 関連 API の概要と使用例

*1:C# 実装も作る予定

*2:というより、仕様書やチュートリアルをろくに発見できなかった・・・

*3:PHP では文字列をキーに持つ連想配列と、ただの配列を区別しない

*4:配列の要素のキーに、is_numeric な文字列しかないなら [ ... ] になるし、numeric でない文字列が混ざっているなら { ... } になる。0 要素なら [ ... ] になる。

*5:実際には phpize コマンドによって処理された結果生成される関数なのかもしれない

*6:json_encode にて、zend_parse_parameters の戻り値を == FAILURE している点から伺える

*7:json_encode に渡したデータもここに渡されている

*8:と言いつつ、つい他にも言及してしまいそうではある。言語処理系は何かと楽しいから困る。

*9:全部大文字な辺りや、他の Zend API の仕様から読み取れる思想からしても、Z_TYPE_P はおそらく関数でなくマクロだろう

*10:IS_LONG など

*11:PHP 6 以前では単に文字列と呼ばれていた。

*12:PHP の文字列は、正確には長さ付きバイト列であるため

*13:速度面の問題もあるかもしれない

*14:PHP 世界では、渡されたデータを再帰的に処理する手続きには _r と付ける慣習があるようだ。有名どころでは print_r というデータを再帰的にダンプする便利な関数がある。

*15:他の部分のソースコードを見るに、zstr といった別の文字列関連の型もあるようだ

*16:emalloc という名前と機能の関数は、Zend API に限らずいろいろなツールや処理系がそれぞれ独自に持っていることが多いようだ。