タグ別アーカイブ: core

phpの配列はどのようにして初期化され実行されるのか

概要

phpなどのLLは、記述するだけでコンパイルなしに実行されますがその中身はどうなっているのでしょうか。
今回は配列を例にとって、実際にphp処理系をおってみます。
主に字句解析、構文解析の実装について順を追って解説していきたいと思います。

zend処理系

まずはphpの処理系について全体像を軽く解説します。
ドキュメントも結構まとまっておりこの辺りなど非常に参考になります。(4年前ですがこの勉強会行きたかったな・・・)

さて、話を戻しますがphpのコアな部分はzend処理系と呼ばれる実装によって成り立っています。
zend frameworkというフレームワークが存在しますが、そちらのフレームワークとしてのzendではなくてphpの初期の発展に寄与したグループのzendです。

zend処理系ではphpのコードから実際に処理を実行するまでに下記のフェーズを実行します。

  1. 字句解析
  2. コードを字句(トークン)に分解します。トークンとは文が構成される最小単位のことです

  3. 構文解析
  4. 分解されたトークンを実際にどういった命令であるかということを解釈します

  5. OPcodeに翻訳
  6. 解釈した公文をOPcodeという中間コードに翻訳します

  7. 実行
  8. 実際にOPcodeを実行します

通常のアプリケーション開発なんかで言うとOPcodeなどは聞いたことがある方も多いのではないでしょうか。
有名どころではOPcacheなどがありますね。

OPcacheはその名の通りOPcodeをキャッシングします。
phpのコードに対して字句解析・構文解析を実行することで中間コードを生成するのですが、同じコードを毎回解析するのはかなり無駄なコストですね。
この中間コードをキャッシングすることで実行性能を上昇させることを目的とした機能です。
余談ですがこちらはphp5.5以降では標準搭載されるようになっていますね。

実際にはその前のフェーズとして字句解析・構文解析が行われます。
これらはごく普通にphpアプリケーションを開発しているだけだとあまり関わることがありません。今回はこちらについて掘り下げていきたいと思います。

字句解析

まず字句解析について。先に簡単に説明したようにコードを字句(トークン)に分解する作業です。

イメージしやすいように下記のサンプルコードを実行してみます。

<?php
$tokens = token_get_all('<?php
$hoge = array();');
foreach ($tokens as $i => $token) {
	echo is_array($token) ? token_name($token[0]) : $token;
	echo PHP_EOL;
}

echo PHP_EOL;

$tokens = token_get_all('<?php
$fuga = [];');
foreach ($tokens as $i => $token) {
	echo is_array($token) ? token_name($token[0]) : $token;
	echo PHP_EOL;
}

token_get_allはphpコードを引数として受け取り、そのコードをトークンレベルまで分解します。
またtoken_nameは分解されたトークンにzend処理系で管理している enum yytokentype を取得します。詳しくは zend_language_parser.c などを参照しましょう。

さて、サンプルコードの実行結果は下記のようになります。

T_OPEN_TAG
T_VARIABLE
T_WHITESPACE
=
T_WHITESPACE
T_ARRAY
(
)
;


T_OPEN_TAG
T_VARIABLE
T_WHITESPACE
=
T_WHITESPACE
[
]
;

T_OPEN_TAGはphpの開始タグを表すenum定数になります。またT_WHITESPACEはホワイトスペースを表すenum定数になります。
このようにphpのセンテンスをzend処理系で用意されたトークンに細かく分解していきます。

さらにここでarrayはT_ARRAYにトークン解析されていることがわかります。またそのあとのカッコについてはそのままになっていることがわかりますね。
これはそれ自体で意味を持っているメタ文字になるからです。

各種zend処理系での予約後に関してはトークン解析によってenumに置き換えられ、それ以外はメタ文字としてそのまま認識されることになります。

構文解析

続いて解析できたトークンを意味のある文として解釈します。zend処理系ではこの構文解析の実装とbisonと呼ばれるgnuツールを利用しています。bisonはBNFを基本とした亜種で文法を表現します。

bisonについてはこちらでかなり詳しく説明しておられるので参考にしてみると良いでしょう。

bisonでは終端トークン(最小単位のトークン)を組み合わせて文として認識できる構成を定義していきます。
zend処理系における実装はzend_language_parser.yに記述されておりますので確認してみてください。

例えば下記のようなコードが合った時

<?php
$a = 'str';

先に述べたようにこれらはまずトークン解析され、続いてbisonによって記述された文に一致するパターンを検索します。
もし文が適合すれば処理しますし、仮に文が解釈できないようであればパースエラーとして実行されないで終了します。
bison(およびBNF記法)の強力なところはそれぞれの文章を再帰的に表現することや、文の定義に他の文を含むことができるので非常に柔軟に文を定義できるところにあります。

arrayの文を確認する

構文解析まで理解できたところで実際にphpではarrayというセンテンスをどのように解釈して処理系が動作しているのかというところを追いかけてみます。

例えば下記のようなphpコードを対象として取り上げてみてみましょう。

$a = array();

字句解析の項目で解説したようにarrayという文字列はT_ARRAYとしてトークナイズされます。bisonの定義ファイルからこれの定義を探してみます。

%token T_ARRAY           "array (T_ARRAY)"

すると上記のような項目が見つかりました。これはトークンT_ARRAYを定義していることを表します。
続いてT_ARRAYを文として定義している箇所(=構文解析を行っていると思われる箇所)を探してみましょう。

combined_scalar:
		T_ARRAY '(' array_pair_list ')' { $$ = $3; }
	|	'[' array_pair_list ']' { $$ = $2; }
;

見つかりましたcombined_scalarという文が定義されており、トークンとしてT_ARRAYが使用されていることから、これが配列の初期化としての文であることが理解できます。

bisonではパイプで区切ることで複数の文を定義することができます。
よって二つ目の文についてはPHP5.4以降で新たに定義されたブラケット(カギカッコ)による配列の定義が確認できますね。
カギカッコの中は構文により呼び出されるc言語による実装です。$$は戻り値を示しており、$xはx番目にマッチするトークンを表しています。

さてここで先に述べたようにbison(BNF)では文を再帰的に定義できますし、文の定義に他の文の定義を含むことができます。
ここではarray_pair_listという文が定義されていることが確認できるので更に文の定義を追いかけます。

array_pair_list:
		/* empty */ { zend_do_init_array(&$$, NULL, NULL, 0 TSRMLS_CC); }
	|	non_empty_array_pair_list possible_comma	{ $$ = $1; }
;

non_empty_array_pair_list:
		non_empty_array_pair_list ',' expr T_DOUBLE_ARROW expr	{ zend_do_add_array_element(&$$, &$5, &$3, 0 TSRMLS_CC); }
	|	non_empty_array_pair_list ',' expr			{ zend_do_add_array_element(&$$, &$3, NULL, 0 TSRMLS_CC); }
	|	expr T_DOUBLE_ARROW expr	{ zend_do_init_array(&$$, &$3, &$1, 0 TSRMLS_CC); }
	|	expr 				{ zend_do_init_array(&$$, &$1, NULL, 0 TSRMLS_CC); }
	|	non_empty_array_pair_list ',' expr T_DOUBLE_ARROW '&' w_variable { zend_do_add_array_element(&$$, &$6, &$3, 1 TSRMLS_CC); }
	|	non_empty_array_pair_list ',' '&' w_variable { zend_do_add_array_element(&$$, &$4, NULL, 1 TSRMLS_CC); }
	|	expr T_DOUBLE_ARROW '&' w_variable	{ zend_do_init_array(&$$, &$4, &$1, 1 TSRMLS_CC); }
	|	'&' w_variable 			{ zend_do_init_array(&$$, &$2, NULL, 1 TSRMLS_CC); }
;

possible_comma:
		/* empty */
	|	','
;

array_pair_listの文を確認するとempty(0個のトークンで構成される)またはnot_empty_array_pair_list possible_commaで構成されることがわかります。

possible_commaの文定義を確認するとコンマもしくはなにもなし、と定義されることがわかります。PHPの配列では末尾にコンマをつけてもつけなくても認識してくれますが、それはこの文定義によるものだということが理解できますね。

さらにnot_empty_array_pair_listを確認してみます。定義がそれぞれありますがphpの多様な配列定義を文として定義しています。
例えばphpのセンテンスとして下記のようなものがありますが、それぞれがこの構文として定義されています。

<?php
$array1 = array('key' => value);
$array2 = array(1, 2);
$array3 = array();

最終的にzend_do_init_arrayというcの関数が呼び出されていることがわかります。
今回は構文解析からarrayの処理系の解釈を追うことが目的ですので、zend_do_init_arrayについては次の機会に取り上げたいと思います。

まとめ

いかがでしたでしょうか。
意外とおってみるとそんなに難しくないことがわかります。これもbisonなど先人が作り出してきた便利なツールによるものですね。