エクステンションTweetこんにちは、久保田です。2/16に行われたZendEngine勉強会で、「拡張ライブラリでなるべく簡単に構文を追加する方法」というタイトルで喋ってきました。勉強会を開催したyoyaさん、会場提供して頂いたGREEさんありがとうございます。ZendEngineとは、PHPの内部で利用されている仮想マシン(VirtualMachie)のことです。これを知ることは、拡張ライブラリを実装したり、PHPの内部実装に精通するためには避けては通れない道です。この記事では、ZendEngine勉強会で喋った内容をもう少し詳しくテキストにして解説します。はじめにPHPの拡張ライブラリには、単にC/C++言語で記述されたライブラリをPHPでも使えるようにする拡張ライブラリと、PHPそのものの振る舞いを変える拡張ライブラリの二種類があります。前者で有名なのは、画像を加工変換合成するImageMagickやGD、マルチバイト文字列を扱うmbstringモジュール、JSONデコードエンコードを行うためのjsonモジュールなどです。後者のPHPそのものの振る舞いを変える拡張ライブラリの例としては、コンパイル結果をキャッシュするAPC、デバッグ支援や関数トレースやコードカバレッジ取得などを行ってくれるXdebugなどが主に挙げられます。この記事では、後者のPHPそのものの振る舞いを変える拡張ライブラリについて記述しつつ、拡張ライブラリからの文法拡張はどのように行えば良いのかについて説明します。記事の前半では、後者の拡張ライブラリがどうやってPHPの振る舞いを変化させていくのかを解説する前に、まずは言語処理系としてのPHPを眺めつつPHPのソースコードがどのように実行されていくのかについてのおおまかな流れを解説します。次に、APCやXdebugがどのようにしてPHPの振る舞いを変化させているのかについての基本的な仕組みを解説していきます。ここでは、ZendEngineに予め用意されている拡張ポイントにどんなものがあるのかを解説します。記事の後半では、PHPにXMLリテラルなどの独自文法を提供するXHP拡張ライブラリを紹介します。ここではXHP拡張の動きを解説しつつXHP拡張を改造して実際にPHPに独自文法を追加します。言語処理系としてのPHPの大まかな仕組み拡張ライブラリがどのようにPHPの振る舞いを変更しているかを知る前に、まずは言語処理系としてのPHPの大まかな仕組みを説明します。以下のようなPHPのソースコードがあるとします。world";このソースコードをPHPに与えると当然ながら以下のように出力されます。wolrdこのソースコードがPHPに与えられ、実行されるプロセスを図に表すと以下になります。VM用コードにコンパイル、VMによる実行、という四つのフェーズに大別されています。VMというのはVirtualMachine(仮想機械)のことで、PHPにおいてはZendEngineです。以下ではこれらの個別のフェーズについて説明していきます。字句解析ソースコードをトークン(字句)のリストに切り分けるフェーズです。具体的にPHPのソースコードがどうトークンの列に分解されるかは、token_get_all()という関数を使って確かめられます。"\n";}これを実行すると以下になります。;トークンに種類を知るには、PHPマニュアルのパーサトークンの一覧を見ましょう。PHP内部での字句解析の実装を知るには、PHPソースコードのZend/zend_langugage_parser.lを覗けます。字句解析には字句解析のためのライブラリであるre2cが使われているのでそれのマニュアルも合わせて見るとよいでしょう。次の構文解析のフェーズでは、字句解析の結果得たトークンの列からどの構文が利用されているかを解析していきます。ここではトークンの列が対応するPHPの文法としてif文であったりクラスの宣言であったり様々な構文が解析されていきます。不正な文法が与えられた場合はここでパースエラーとなって弾かれます。PHPはこの構文解析にbisonと呼ばれるパーサジェネレータを利用しています。PHPではZend/zend_language_parser.yにそれを使った文法の定義が載っています。これもbisonのマニュアルと共に読んでいくとよいでしょう。VM用コードにコンパイルVM用のコードはしばしばオペコードと呼ばれます。PHP内部のコンパイラはソースコードをオペコードにコンパイルしていきます。ここで初めてソースコードはZendEngineが実行出来るコードに変換されます。ソースコードをいちいち内部のVM用のコードに変換してから実行するのは高速化のためです。Pythonなど多くのスクリプト言語では高速化のためにソースコードを内部のVM用のコードに実行時コンパイルしてからVMで実行します。MagazineのVMってなんだろうという記事で解説されています。これはRubyのYARVを説明するために記述されているものですが、言語の内部VMの一般的な話として参考になります。このVM用コードへのコンパイラ部分のソースコードは、Zend/zend_compile.cに記述されています。これらの関数は構文解析フェーズでZend/zend_language_parser.yから呼ばれているものが多いので合わせて読むと理解しやすいです。最後に、ZendEngine用にコンパイルされたコードをZendEngineが実行します。ZendEngineのオペコードの種類を知るにはマニュアルのZendEngine2のオペコード一覧を参照します。通常のPHPソースコードからどういったオペコードの列が生成されるかは、parsekitモジュールやvldモジュールを用いてみることができます。ここではparsekitモジュールを用いて以下のように生成されるオペコードを確認できます。ZEND_ECHOやZEND_JMPZなど行の先頭にあるのがオペコードの種類で、それに続くのがオペコードへの引数です。UNUSED予めZendEngineに用意されている拡張ポイント以上、言語処理系としてのPHPがどのようにソースコードを実行しているのかをおおまかに解説しました。PHPそのものの振る舞いを変える拡張ライブラリでは、以上の4つのフェーズで行われる処理を追加、変更するものが多いです。具体的には、APCでは「字句解析+構文解析+VM用のコードにコンパイル」という三つのフェーズをキャッシュによって省略しようとします。Xdebugではテストカバレッジの計測や関数トレースのためにコンパイルフェーズと実行フェーズに振る舞いを追加します。これらの拡張ライブラリでは、PHPの振る舞いを変更するためのZendEngineに予め用意されている拡張ポイントを利用しています。zend_executeという関数ポインタの変数が拡張ポイントとして用意されます。これらの関数ポインタにはあらかじめオリジナルのZendEngineの関数が設定されていますが、拡張ライブラリからはこれを自由に設定しなおすことができます。これらに設定されるデフォルトの振る舞いは以下になります。ファイル名からそのファイルのソースコードを取得しVM用のオペコードにコンパイルする。文字列からそのファイルのソースコードを取得しVM用のオペコードにコンパイルする。evalを使う際に利用される。インタラクティブシェル。VM用にコンパイルされたオペコードを実行する。zend_compile_stringにデフォルトで設定される関数は、上述したPHPの実行の流れの4つのフェーズのうち、「字句解析+構文解析+VM用コードにコンパイル」という三つのフェーズを担当します。拡張ポイントはこれ以外にもあるのですが、筆者自身が全ての詳細を把握してないのでとりあえずはこの三つを説明しました。これらの拡張ポイントが実際にどのように利用されているかを知るには、APCなどの拡張ライブラリのソースコードを眺めてみると良いでしょう。PHPに構文を追加するこれまでPHPの大まかな実行の流れと拡張ポイントについて記述しました。これらのことを踏まえると、PHPにオリジナルの構文を拡張ライブラリから追加する方法というものが見えてくると思います。zend_compile_fileという二つの関数ポインタに自分が作成した関数を設定すれば、「字句解析+構文解析+VM用コードにコンパイル」という三つのフェーズの振る舞いを取り替えられます。ここで自分でパーサやコンパイラを実装することで、文法を自由にカスタマイズできます。実際に、それを行って様々な文法拡張や無名関数の導入などを行うQIQという拡張ライブラリも存在します。PHP5.3では無名関数が導入されましたが、この拡張ライブラリは無名関数のないPHP5.2時代に無名関数などを拡張ライブラリレベルで導入します。このライブラリではパーサの拡張のために前述したzend_compile_fileを拡張ライブラリから設定して構文解析器を取り替えることで実現しています。実装の中身については作者のrskyさんによる解説記事や資料を見ましょう。問題点:めんどうくさいというわけでやっと拡張ライブラリからどのようにして文法拡張できるのかだんだん流れを掴めてきたと思います。ひとつ問題があります。上述したように、自分でパーサとコンパイラを実装すればオリジナルの文法を導入できるというのはいいのですが、ほんのちょっとPHPにオレオレ構文を追加したい、という時にPHP本体とほんの少し違うパーサやコンパイラをまるごと実装しなければならない、というのはなんとなく手に余る印象を受けます。したハックでPHPに独自文法を導入する方法は無いのでしょうか?XHP拡張ライブラリここでXHP拡張ライブラリを紹介します。facebookによるオープンソースプロジェクトとして公開されているこの拡張ライブラリは、PHPに以下のような拡張文法を導入します。これらの例はドキュメントから引用したものです。$list->appendChild(<li>{$item}</li>);}ご覧のとおり、PHPにXMLリテラルを埋め込める拡張文法を提供しています。XMLリテラルが生成するオブジェクトのクラスは以下のように宣言することができます。...}文法だけではなく、属性や要素内にPHPの式をおくと勝手にエスケープしてくれます。XHP拡張ライブラリの仕組みXHP拡張がどう動いているかのドキュメントの冒頭からこの拡張ライブラリが何をやっているのかを引用します。metaprogramming.このXHP拡張はcompile_file関数とcompile_string関数をフックする。XHPはこれらの関数の呼び出しを横取りし、プリプロセスしてから全てのコードを実行する。XHPのコンパイル段階では、PHPのスーパーセットの文法を解釈してPHPが理解できるコードを返す。XHPが提供しているものは、自動的なメタプログラミングを通じての糖衣構文である。zend_compile_stringに予め設定されている関数です。実行時コンパイルのデフォルトの振る舞いのことです。XHP拡張ではこのデフォルトの振る舞いに、拡張文法が入ったソースコードを通常のPHPのソースコードに変換するプリプロセスのフェーズを挿入します。自動的なメタプログラミングというのはそれのことです。わかりやすく図で説明すると、通常のPHPが左図のプロセスを経て実行されるのに対して、XHP拡張を読み込むと右図のプロセスを経て実行されるようになります。XHPの場合プリプロセスのフェーズが追加されているのがわかります。XHPが具体的にどうコードを変換するのかを確認する場合は、xhp_preprocess_code関数を以下のように用います。$code['new_code'];実行すると以下になります。7);}XMLリテラルがオブジェクトを生成するコードに変換されています。XHP拡張の良いところとして、PHPのスーパーセットの字句解析器と構文解析器がすでに備わっており、しかも文法拡張にプリプロセスを行うだけという単純さがあるので、これを改造するだけで簡単に独自文法を追加することができます。branif*ck構文というわけでこのXHP拡張ライブラリに改造を施してbrainf*ck構文を追加してみました。brainf*ckは難解プログラミング言語の中ではおそらく最も有名な言語のひとつです。8つの命令しかありませんがチューリング完全で、理論的には普通のプログラミング言語と同じ表現力を持つと言われています。このハックではPHPのコード中にbrainf*ckを実行出来る構文を用意しました。"+"や">"など通常は演算子として用いられますが、XHPのパーサにbrainf*ck構文用の文法定義を追加してbrainf*ckのコードとして実行されるように拡張しています。以下の様にbrainf*ckを実行することができます。------.--------.>+.;これを実行すると以下になります。world!拡張ライブラリのソースコードはgithubにてxhp-brainf-ckという名前で公開しています。ビルドの方法はXHPと同じなのでXHPのドキュメントにあるビルド方法を参考にしてください。通常の拡張ライブラリのビルドの方法とも変わりはないので特に難しくないでしょう。XHPのどの辺を改造しているかはこのコミットログにdiffがあるので見てください。XHPを用いて実行する際はphp-lib/init.phpをincludeするのを忘れないでください。XHP拡張はあくまでPHPに糖衣構文を追加するだけのもので、実際に独自構文に対して新しい振る舞いをさせるにはそれを行うライブラリを読み込まなければなりません。以下に拡張ライブラリ関連の参考になる資料を挙げておきます。公式マニュアルでの処理系コアや拡張ライブラリの解説。byスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。