スキップしてメイン コンテンツに移動

WindowsにおけるC++へのPHP組み込み環境の構築

前回の記事では、C/C++コードへのPerlとRubyの組み込みを扱った。DICEへの組み込みの評価を兼ねていて、当時はPerlを使うことになった。一方で、DICEのWebサーバとしての側面をもっと強調せねばという課題が最近わりと念頭にあり、Webサーバを名乗るからには現在のWeb向けスクリプト言語No1としてのPHPをサポートしていないというのはいかにも心苦しい。PHPはApacheと関連付けて語られることも多い以上、Apacheの代替を目指しているわけではないDICEでサポートする意味も薄いと判断し敬遠してきたという経緯もあったものの、あまりにもPHPの勢いがありすぎるので仕方なくサポートに向けて舵を切ったというわけだ。特に海外では、VBulletinやWordPressといった代表的Webアプリケーションが利用できてようやくそれなりのWebサーバとしてユーザの検討の俎上に載せられることもあるだろう。

PHPの魅力は、言語仕様ではなくバックエンドにある。特に、実行時間を限定できるオプションは有り難い。中をあまり見ていないので実装の詳細は知らないが、実行スレッド以外にスレッドを起動するか、あるいは元々のディスパッチャがディスパッチしたタスクを定期的にチェックするなりして監視していると思われる。WindowsだとTerminateThreadというAPIでスレッドを止めることが出来るけれども、これはリソースリークを起こす不安定なAPIとして知られている。特に、OSのスレッドプールを使っている場合は、スレッドを勝手に止めるとどんな予期しない悪影響が起こるかわかったものではない。PHP側で非ネイティブスレッドの実行を途中停止しているとすれば、PHPを組み込んで使うクライアントコード側としては手間が省けて大いに助かるというわけである。

本記事の環境はVS2005(VC8)である。PHPはPHP 5.25を用いた。PHP組み込みに関する公式の資料はPHPマニュアル第7章に存在する。46節の"Zend API: Hacking the Core of PHP"に変数の操作に関して記述がある。ところが、現時点のPHPマニュアルには、組み込みの際の初期化についてすら解説がない。PHPソースコードを展開した際にphp-5.2.5\sapi\embedフォルダに見つかるphp_embed.hならびにphp_embed.cを参照すると、初期化/終了マクロがあり、初期化の際にsapi_startup関数を呼んでいるのが分かる。これはmain\SAPI.hならびにSAPI.cに定義されており、引数となる構造体_sapi_module_structにコールバックを登録してPHPのSAPI(Server API)を初期化している。そのコールバックの中でも、php_embed_ub_writeがprintなどの出力を受ける関数で、php_embed.c内ではphp_embed_ub_writeを経由してphp_embed_single_writeに至り、標準出力へ出力されている。つまりそこをオーバライドしてやればユーザ側でスクリプトの出力を受け取れるのであり、基本的にはRuby組み込みで使われる方法と同一だが、PHPはSAPIフレームワークの中でインターフェイスをユーザ側に提供している点が異なる。

ユーザコードの概要が掴めたところで、次にPHP組み込みに必要な環境を作らなければならない。先に書いておくが、ここにまず問題がある。PHPのソースコードをダウンロードしてローカルに展開した後に、マニュアルのWindows上でのビルド方法に従ってビルドを行う。win32build.zipbindlib_w32.zipもビルドに必要なGNUツールなどを含んでいるので所定の場所へ展開しておく必要がある。ビルドには、Visual C++ツールのコマンドプロンプトを使用する。今回は組み込みを行うので、WSHを経由してconfigure.jsを呼び出しmakeの設定を行う際に、--enable-embedを引数に加えることになる。

ところが、そのままビルドを始めると、エラーが出てストップしてしまう。エラーコードを見ると、libxmlに問題が起こっているらしい。そこで libxml2をダウンロードしてパスを通すも、やはりビルドが通らない。今度はどうやらiconvが必要なようで、最新版libiconvをダウンロー ドしてみると、なんと以前は入っていたVC++サポートがどういうわけか削除されていた。mingwでビルドせよとあるので試してみたがどうも上手くいかない。しかも、ダウンロード元がGNUだったことを思い出し、これはライセンス的にまずいのではないかと後から気付いた。iconvに由来するコードが PHPのスタティックライブラリに含まれているとすれば、PHP組み込みを行った時点でLGPLに汚染され、ユーザプログラムの配布に支障が出てしまう。 そこで、仕方なく--without-libxmlをconfigure.jsのオプションに加え、XMLサポートを諦めた。調べてみると、iconv自体はPHP 5からPHP本体に組み込まれており、php-5.2.5\ext\iconvを見る限りクリーンルーム実装がなされている。このあたりlibxmlもどうにかできないものかと思うがここでは深く追及しない。

libxml/iconvの問題以外にもビルドプロセスには問題があり、ZLIBの関数も使用できないので--disable-zlibで無効にする。これでようやくphp-5.2.5\Release_TSないしphp-5.2.5\Debug_TSに、アプリケーションにリンクするスタティックライブラリ(php5embed.lib)と実行時に必要なdllファイル(php5ts.dll)が出来上がる。次にVC++の設定として、メニューの「ツール | オプション」からVC++のディレクトリにlibファイルの場所を追加し、さらにプロジェクト設定内のincludeファイルのフォルダにphp-5.2.5\php-5.2.5\sapi\embedphp-5.2.5\Zendphp-5.2.5\TSRMを加える。さらに、PHP_WIN32ZEND_WIN32各シンボルの定義が必要となる。また、実際には、VC8からtime_tが64ビットになっているため_USE_32BIT_TIME_Tも定義しなければクライアントコードのビルドは成功しない。以上でどうにか環境が整ったので、以下のコードがPHP組み込みのサンプルとしてビルド、実行できる。


#include <stdio.h>
#include <tchar.h>

#include <iostream>
#include <string>
#include <cstdio>

#include <php.h>
#include <php_embed.h>

using namespace std;

static int php_ub_write(const char* str, unsigned int str_length TSRMLS_DC)
{
string s(str, str_length);

cout << s;

return str_length;
}

static void php_log_message(char* message)
{
cerr << "php_log_message: " << message << endl;
}

static void php_sapi_error(int type, const char* fmt, ...)
{
va_list va;
va_start(va, fmt);
printf("php_sapi_error: ");
vprintf(fmt, va);
va_end(va);
}

int _tmain(int argc, _TCHAR* argv[])
{
php_embed_module.ub_write = php_ub_write;
php_embed_module.log_message = php_log_message;
php_embed_module.sapi_error = php_sapi_error;

char* argv2[] = {""};

PHP_EMBED_START_BLOCK(0, argv2);

zval* v = NULL;

MAKE_STD_ZVAL(v);
ZVAL_STRING(v, "Hello", 1);
ZEND_SET_SYMBOL(&amp;EG(symbol_table), "hello", v);

zend_eval_string("print \"$hello \";$hello = \"World\";", NULL, "" TSRMLS_CC);

if (v->type == IS_STRING)
{
cout << string(v->value.str.val, v->value.str.len) << endl;
}

PHP_EMBED_END_BLOCK();

return 0;
}



PHP関係のヘッダは他の標準ヘッダの後に置かなければエラーが出るようだ。コードの内容は、上述のように出力関数をオーバライドし、さらに$helloという名称の変数を作成して操作し最終的にHello Worldを表示するというたわいのないものである。PHP_EMBED_START_BLOCKPHP_EMBED_END_BLOCKが初期化、終了のマクロで、内部でzend_first_try/zend_catchのブロックを作っているので変数のスコープに注意しなければならない。前回のPerl組み込みの場合だとPerlでの例外をラップする仕組みを自前で作る必要があったのに対し、PHPの方はZendが面倒を見てくれる点は有り難い。

とはいえ、このようにしてビルドした物だとPHP 5の特徴的機能であるところのXMLがおそらく使用できない事はかなりの損失である。そこで、次に考えられる方法として、PHPの配布バイナリ内のライブラリを利用するという道がある。PHPの配布バイナリには、(一体どうやってビルドしたのかは謎だが)php5embed.libphp5ts.dllが含まれているのである。Dependency Walkerでphp5ts.dllを覗いてみてもCライブラリがスタティックリンクされているので一見使い勝手が良いように見える。ところが、落とし穴があり、デバッグ版だとVC++のdllが異なってくるので、これを使った場合正常にデバッグが出来なくなってしまう。そこで、開発の際には自分でビルドしたlibファイルを使い、リリース版は配布バイナリの方を利用することになる。

しかし、拍子抜けではあるが、最終的にDICEでは上述のようなストレートな組み込み方法は採らなかった。というのも、不透明なライセンスの問題に加え、上述のやり方ではPHPスクリプトやASP(Active Server Pages)の最も有用な機能である、コードブロックのHTML埋め込みが出来ないのである(出来る方法があるのかも知れないがそれは本稿の範囲を超える)。これではこちらで配布するWebサーバに組み込んで利用するという本来の目的にそぐわない。そこで、結局DICEの方にISAPIエクステンションのサポートを入れ、ISAPIを経由してPHPをロードする形に落ち着いた。PHPはSAPIという組み込みインタフェイスを用意しており、PHPの配布バイナリにはISAPIエクステンションのdllが含まれている。この組み込み方法であればライセンス問題は完全にクリーンであり、PHP設定ファイルの読み込みなどの付随的処理もPHP側で全て自動的に行ってくれる。PHPはかなり素直なISAPIアクセスしか行わないので、別にwebサーバへの組み込み用途ではなくても、Windows上で ライセンスが気になる場合等はPHPそのものの組み込みの代わりにISAPIを経由してコントロールするのも一つの有力な選択肢ではないかと思う。他方で、配布を目的とせずサーバ側でのみ利用する場合はライセンスに関する問題はもちろん発生しないため、最初に取り上げた組み込み手順もそれはそれで有用となる局面もあるだろう。

コメント