PHP
downloads | documentation | faq | getting help | mailing lists | reporting bugs | php.net sites | links | conferences | my php.net

search for in the

TSRM API> <PHP 拡張モジュールの作者用のストリーム API
Last updated: Fri, 29 Aug 2008

view this page in

Zend API: PHP のコアをハックする

導入

Those who know don't talk. (知ってる人は教えようとしない)

Those who talk don't know. (教えてる人はあまり知らない)

時には、「あるがままの」PHP では要件を満たさないことがあります。平均的な ユーザがこのような事態に陥ることはまずありませんが、プロがアプリケーションを 作成していると、速度面や機能面ですぐに PHP の限界に達してしまいます。 言語の制限により、新機能が常にネイティブ実装されるとは限りません。 そんな場合には、たった 1 行のコードのために大げさなライブラリを利用 しなければならず非常に不便です。このような PHP の欠点に打ち勝つための なんらかの手段が必要です。

もしこの域に達したなら、PHP の心臓部に手を触れ、その中身(PHP を動かして いる C のコード)を探ってみるとよいでしょう。

警告

この情報は、現在はかなり時代遅れになっています。 PHP 4 の初期バージョンで使用されていた、 初期の ZendEngine 1.0 の API しか網羅していない部分もあります。

より新しい情報は、PHP のソースに含まれる各種 README ファイル、 あるいは Zend の Web サイトにある » Internals を参照ください。

概要

「PHP を拡張する」と言うのは簡単ですが、実際に行うのは大変なことです。 PHP は数メガバイトのソースコードからなる成熟したツールに発展して きており、このようなシステムをハックするには、学ばねばならないことが あります。この章を構成するにあたり、我々は「実際に作業することによって 学んでもらう」という手法をとることにしました。これは科学的とは言えないし プロフェッショナルな方法でもありません。しかし、楽しく学ぶことが できて最終的によい結果に終わると考えたのです。 以下の節では、最も基本的な拡張モジュールをお手軽に作成する方法を 学びます。その後で、Zend API の高度な機能について学びます。 もうひとつの方法としては、機能・デザイン・ヒント・裏技などを 一気に学ぶ、つまり実践的な手法の前にまず大きな絵の全体を見せてしまう というものがあります。何の小細工もせずにすむという点で、こちらの 方法のほうが「よりよい」ものではあるのですが、この方法は 非常にフラストレーションがたまり、時間と気力をかなり消費するもの でもあります。そのため、我々は前者の方法をとることにしたのです。

この章では PHP の内部動作についてできる限りの知識を伝えるように 心がけています。しかしながら、いつどんなときでも 100% 動作する 完璧な PHP 拡張モジュール作成ガイドを提供するのは不可能であることを 知っておいてください。PHP は巨大で複雑なパッケージであるので、 実際に手を動かしながら勉強していかないと内部構造は理解できないでしょう。 そのため、実際のソースとともに作業を進めていくことを推奨します。

Zend とは何 ? そして PHP とは何 ?

Zend という名前は PHP のコアとなる言語エンジンを 指します。PHP は、外部から見た際のシステム全体を 指す単語です。最初はすこし紛らわしく感じるかもしれませんが、これは そんなに複雑な話ではありません (以下を参照ください)。 Web スクリプトのインタプリタを実装するには、3 つのパートが必要です。

  1. インタプリタ (interpreter) は 入力コードを解析し、変換し、そして実行します。

  2. 機能 (functionality) は、言語の機能 (関数など) を実装します。

  3. インターフェイス (interface) は、 Web サーバなどと会話をします。

1 のすべてと 2 の一部を担当するのが Zend で、2 および 3 を 担当するのが PHP です。この 2 つをあわせることで PHP パッケージが 完成します。Zend 自身は言語のコアのみを受け持つもので、 事前に定義された関数などの PHP の基本部分のみを実装します。 PHP が、この言語の優れた可能性を作り出しているすべての 拡張モジュールを受け持ちます。
PHP の内部構造

これ以降のセクションでは、PHP を拡張するにはどこをどのように変更すれば よいのかを説明します。

拡張の可能性

先ほど 示したとおり、PHP を拡張するには 3 つの方法があります。それは 外部モジュール・組み込みモジュール・Zend エンジン の 3 つです。以下の節で、これらそれぞれについて考えます。

外部モジュール

外部モジュールは、スクリプトの実行時に dl() 関数を使用して読み込みます。この関数は、ディスクから共有オブジェクトを 読み込んで、呼び出し元のスクリプトからその機能を使用できるように します。スクリプトが終了すると、外部モジュールはメモリから削除 されます。この方法には利点も欠点もあります。それを以下の表にまとめます。

利点 欠点
PHP をコンパイルせずに外部モジュールを使用できる。 スクリプトが実行されるたび (アクセスされるたび) に 共有オブジェクトを読み込む必要があり、速度が非常に遅くなる。
その機能を「外部にまかせる」ことにより、PHP のサイズを 小さく抑えられる。 外部の追加ファイルによってディスクを散らかしてしまう。
    そのモジュールの機能を使用するすべてのスクリプトが dl() をコールするか、あるいは php.ini の中の extension タグを変更する (この方法が適切でない場合もある) 必要がある。
要するに、外部モジュールが有用なのは以下のような場合です。 サードパーティの製品・PHP のちょっとした機能拡張で、めったに 使われないもの・単なるテスト目的で作成するものなど。 追加機能を手っ取り早く開発するには、外部モジュールが最適です。 頻繁に使用するものであったり大規模な実装であったり、あるいは 複雑なコードの場合などは、欠点が利点を上回ってしまうでしょう。

サードパーティは、php.iniextension タグを使用して PHP に拡張モジュールを 追加することを考えるかもしれません。これらの拡張モジュールは 本体のパッケージとは完全に切り離されます (これは、商用の環境では とても便利な機能です)。商用製品を配布する場合は、単に追加モジュール のみを含むディスクやアーカイブを出荷すればよいのであり、他の 拡張モジュールが使用できなくなるような PHP バイナリを作成する 必要がなくなります。

組み込みモジュール

組み込みモジュールは、PHP のコンパイル時に直接組み込まれ、PHP プロセスと一緒に動作します。すべてのスクリプトで、その機能が 使用可能となります。外部モジュールの場合と同様、組み込みモジュールに ついても利点と欠点があります。それを以下の表にまとめます。

利点 欠点
いちいちモジュールを読み込む必要がない。 なにもしなくてのその機能が使用できる。 組み込みモジュールを更新するには PHP を再コンパイル する必要がある。
別の外部ファイルでディスクを散らかすことはない。 すべては PHP のバイナリに組み込まれる。 PHP バイナリのサイズが大きくなり、メモリ消費量も増える。
比較的変更の少ない安定した関数のライブラリで、平均以上の速度を 要求するものであったり、あるいは多くのスクリプトから頻繁に 使用されるようなライブラリなどの場合は、組み込みモジュールに するのが最適です。PHP を再コンパイルしなければならないという 問題は、それによって得られるスピードや使いやすさにくらべたら たいしたことはありません。しかし、小さな変更が頻繁に繰り返される ような場合には、組み込みモジュールは不適当です。

Zend エンジン

もちろん、拡張機能を Zend エンジンに直接組み込むこともできます。 言語そのものの振る舞いを変更したい場合、あるいは言語のコアに 特別な関数を直接組み込む必要がある場合などには、この方式がよいでしょう。 しかし、一般的には Zend エンジンを変更することは避けるべきです。 ここを変更してしまうとその他の部分で非互換性が発生する可能性があり、 特別に変更された Zend エンジンに対応できる人は誰もいなくなるでしょう。 変更内容を PHP のソースから切り離すことができなくなりますし、 また「公式」なソースリポジトリからアップデートを行うと、変更内容が 上書きされてしまいます。そのため、この手法はよくないものと 考えられています。使用することはめったにないため、この文書では この手法については取り上げません。

ソース配置

注意: この章の残りの部分に進む前に、お好みの Web サーバの (変更されていない) ソースを取得しておきましょう。ここでは、Apache (» http://www.apache.org/ で取得できます) を使用します。また、もちろん PHP のソース (» http://www.php.net/ にあります - 言うまでもないですよね?) も必要です。
PHP の動作環境を自分でコンパイルして作成できるようにしておいて ください! この方法についてはここでは触れませんが、この章の内容を 学習しようとするのなら、最低限知っておくべき基本的な内容です。

コードの内容について説明する前に、PHP のファイルを探索する助けになるよう ソースツリーの内容に慣れておくべきです。これは、拡張モジュールを 開発したりデバッグしたりする際の必須技能です。

以下の表では、主なディレクトリの内容について説明しています。

ディレクトリ 内容
php-src PHP 本体のソースファイルおよびヘッダファイル。 PHP の API 定義やマクロなどはここにあります (重要)。 それ以外のものは、このディレクトリの下位階層にあります。
php-src/ext 動的モジュール、組み込みモジュールのソース置き場。 デフォルトでは、PHP 本体のソースツリーに統合された「公式」 モジュールが配置されています。PHP 4.0 以降、これらの標準 拡張モジュールを (そのモジュールがサポートしていれば) 動的モジュールとしてコンパイルすることが可能となりました。
php-src/main このディレクトリには PHP 本体のマクロや定義があります (重要)。
php-src/pear PHP Extension and Application Repository (PEAR) のディレクトリです。 ここには PEAR のコアファイルが含まれます。
php-src/sapi さまざまなサーバ用の抽象化レイヤのコードを含みます。
TSRM Zend および PHP の "Thread Safe Resource Manager" (TSRM) の場所です。
ZendEngine2 Zend エンジンのファイルがあります。この中で、Zend の API 定義やマクロなどのすべてが見つけられるでしょう (重要)。

PHP パッケージに含まれるすべてのファイルについて取り上げることは この章の範囲を超えています。しかし、以下のファイルについては 詳しく見ておくべきでしょう。

  • PHP の main ディレクトリにある php-src/main/php.h。 このファイルには PHP のマクロおよび API 定義の大半が含まれています。

  • Zend ディレクトリにある php-src/Zend/zend.h。 このファイルには Zend のマクロおよび定義の大半が含まれています。

  • これもまた Zend ディレクトリにある php-src/Zend/zend_API.h。 ここでは Zend の API を定義しています。

これらのファイルからインクルードされているいくつかのファイルについても 見ておきましょう。例えば、Zend エンジンの実行や PHP の初期化ファイルの サポートに関連するファイルが含まれます。これらのファイルを読んだ後で、 パッケージ全体を見回し、ファイルやモジュールの相互依存性 - 各ファイル・モジュールがお互いにどのようにかかわりあっているのか、 どのようにお互いを使用しているのか - を調べましょう。これにより、 PHP のコーディングスタイルにも慣れることができます。PHP を 拡張しようと思うなら、早いうちにこのスタイルに適応すべきです。

拡張規約

Zend は、ある規約に基づいて構築されています。 この標準規約を破ることを避けるため、 以下の節で説明する規則を守らなければなりません。

マクロ

重要なタスクのほぼ全てについて、Zend では便利なマクロを定義しています。 以下の表および図で、基本的な関数・構造体およびマクロについて 説明しています。マクロ定義のほとんどは zend.h あるいは zend_API.h にあります。この章を勉強したあとで、 これらのファイルをじっくり読んでみることをお勧めします (もちろん今この場で読んでもよいのですが、 まだこの段階ではすべてを理解することはできないでしょう)。

メモリ管理

特にサーバソフトウェアにとって、リソース管理は重大な問題です。 メモリは最も貴重なリソースのひとつなので、 メモリ管理には最大限の注意を払わねばなりません。 メモリ管理の一部は Zend によって抽象化されており、 この抽象化を使用すべきなのは明白です。この抽象化を使用することにより、 Zend はすべてのメモリ割り当てを完全に制御できるようになります。 そのブロックが使用中なのかどうかを Zend が判断し、 未使用のブロックや参照されていないブロックを自動的に開放することで メモリリークを防ぐことができます。 このために使用する関数を、以下の表にまとめます。

関数 説明
emalloc() malloc() の代わりに使用します。
efree() free() の代わりに使用します。
estrdup() strdup() の代わりに使用します。
estrndup() strndup() の代わりに使用します。 estrdup() より高速で、バイナリセーフです。 複製する文字列の長さが事前にわかっている場合には、 この関数を使用することを推奨します。
ecalloc() calloc() の代わりに使用します。
erealloc() realloc() の代わりに使用します。
emalloc() および estrdup()estrndup()ecalloc()erealloc() は、内部メモリを確保します。efree() は、これらの関数で確保したブロックを開放します。 e*() 関数が管理するメモリは、 現在のプロセス内でローカルであるものとして扱われます。 このプロセスによって実行されているスクリプトが終了すると、 すぐにメモリが破棄されます。
警告

スクリプトの終了後も残り続けるメモリを確保するために、 malloc() および free() を使用することも可能です。しかし、これらの関数を使用するのは Zend API がどうしてもそれを要求している場合に限定し、 最大限の注意を払うようにしてください。それ以外の場合に使用すると、 メモリリークが発生する恐れがあります。

Zend は、マルチスレッド Web サーバをサポートするための スレッドセーフなりソース管理機能も提供しています。 この機能を使用する場合は、複数スレッドを同時に実行できるようにするため、 すべてのグローバル変数をローカルの構造体に割り当てなければなりません。 この文書が書かれた時点では Zend のスレッドセーフモードはまだ完成していません。 そのため、この文書ではこれ以上この機能について取り上げません。

ディレクトリ関数およびファイル関数

Zend モジュール内では、以下のディレクトリ関数およびファイル関数を使用しなければなりません。 これらの関数の機能はそれぞれ対応する C 関数と同じですが、 さらにスレッドレベルでの仮想実行ディレクトリがサポートされています。

Zend 関数 標準 C 関数
V_GETCWD() getcwd()
V_FOPEN() fopen()
V_OPEN() open()
V_CHDIR() chdir()
V_GETWD() getwd()
V_CHDIR_FILE() ファイルのパスを引数として受け取り、 現在の実行ディレクトリをそのファイルのディレクトリに移動します。
V_STAT() stat()
V_LSTAT() lstat()

文字列の処理

Zend エンジンでは、文字列はその他の値 (整数値、論理値など) と少し異なる方法で処理されます。 これらの値を保存するために、追加のメモリを確保する必要はありません。 関数から文字列を返したい場合は、新しい文字列変数を シンボルテーブルかそれに類似のものに登録します。 その文字列が使用するメモリは、先ほど説明した e*() 関数で事前に確保しておかなければなりません (この段階では、まだあまりピンとこないかもしれません。 とりあえずは頭の片隅に置いておいてください。あとでもう一度説明します)。

複雑な型

配列やオブジェクトのような複雑な型については、扱いかたが異なります。 Zend はこれらの型を扱うための API を提供しており、 これらの型はハッシュテーブルとして保存されます。

注意: これ以降のサンプルソースでは、 読みやすさを考慮して integer などの単純な型のみを使用します。 より高度な型を作成する方法については、この章の後半で説明します。

PHP の自動ビルドシステム

PHP 4 には、非常に柔軟な自動ビルドシステムがあります。 すべてのモジュールは、ext ディレクトリ以下に配置されています。各モジュールは、 モジュール自身のソースに加えて config.m4 というファイルを持っています。 これは拡張モジュールの設定用のファイルです (» http://www.gnu.org/software/m4/manual/m4.html を参照ください)。

これらの全てのファイルの雛形および .cvsignore は、ext ディレクトリ内にある ext_skel というシェルスクリプトで作成できます。 作成したいモジュールの名前を、スクリプトの引数として渡します。 このスクリプトは引数と同じ名前のディレクトリを作成し、 適切な雛形ファイルを作成します。

順を追って見ていくと、この手順は次のようになります。

:~/cvs/php4/ext:> ./ext_skel --extname=my_module
Creating directory my_module
Creating basic files: config.m4 .cvsignore my_module.c php_my_module.h CREDITS EXPERIMENTAL tests/001.phpt my_module.php [done].

To use your new extension, you will have to execute the following steps:

1.  $ cd ..
2.  $ vi ext/my_module/config.m4
3.  $ ./buildconf
4.  $ ./configure --[with|enable]-my_module
5.  $ make
6.  $ ./php -f ext/my_module/my_module.php
7.  $ vi ext/my_module/my_module.c
8.  $ make

Repeat steps 3-6 until you are satisfied with ext/my_module/config.m4 and
step 6 confirms that your module is compiled into PHP. Then, start writing
code and repeat the last two steps as often as necessary.
この手順により、先ほど説明したファイルが作成されます。 新しく作成したモジュールを自動ビルドシステムに組み込むには、 buildconf を実行しなければなりません。 これは、ext ディレクトリ内を検索し、 見つかった全ての config.m4 ファイルをもとにして configure スクリプトを再作成します。

Zend API: PHP のコアをハックする に示すデフォルトの config.m4 は、すこし複雑です。

例1 デフォルトの config.m4

dnl $Id: build.xml,v 1.3 2007/11/05 13:50:20 takagi Exp $
dnl config.m4 for extension my_module

dnl Comments in this file start with the string 'dnl'.
dnl Remove where necessary. This file will not work
dnl without editing.

dnl If your extension references something external, use with:

dnl PHP_ARG_WITH(my_module, for my_module support,
dnl Make sure that the comment is aligned:
dnl [  --with-my_module             Include my_module support])

dnl Otherwise use enable:

dnl PHP_ARG_ENABLE(my_module, whether to enable my_module support,
dnl Make sure that the comment is aligned:
dnl [  --enable-my_module           Enable my_module support])

if test "$PHP_MY_MODULE" != "no"; then
  dnl Write more examples of tests here...

  dnl # --with-my_module -> check with-path
  dnl SEARCH_PATH="/usr/local /usr"     # you might want to change this
  dnl SEARCH_FOR="/include/my_module.h"  # you most likely want to change this
  dnl if test -r $PHP_MY_MODULE/; then # path given as parameter
  dnl   MY_MODULE_DIR=$PHP_MY_MODULE
  dnl else # search default path list
  dnl   AC_MSG_CHECKING([for my_module files in default path])
  dnl   for i in $SEARCH_PATH ; do
  dnl     if test -r $i/$SEARCH_FOR; then
  dnl       MY_MODULE_DIR=$i
  dnl       AC_MSG_RESULT(found in $i)
  dnl     fi
  dnl   done
  dnl fi
  dnl
  dnl if test -z "$MY_MODULE_DIR"; then
  dnl   AC_MSG_RESULT([not found])
  dnl   AC_MSG_ERROR([Please reinstall the my_module distribution])
  dnl fi

  dnl # --with-my_module -> add include path
  dnl PHP_ADD_INCLUDE($MY_MODULE_DIR/include)

  dnl # --with-my_module -> chech for lib and symbol presence
  dnl LIBNAME=my_module # you may want to change this
  dnl LIBSYMBOL=my_module # you most likely want to change this 

  dnl PHP_CHECK_LIBRARY($LIBNAME,$LIBSYMBOL,
  dnl [
  dnl   PHP_ADD_LIBRARY_WITH_PATH($LIBNAME, $MY_MODULE_DIR/lib, MY_MODULE_SHARED_LIBADD)
  dnl   AC_DEFINE(HAVE_MY_MODULELIB,1,[ ])
  dnl ],[
  dnl   AC_MSG_ERROR([wrong my_module lib version or lib not found])
  dnl ],[
  dnl   -L$MY_MODULE_DIR/lib -lm -ldl
  dnl ])
  dnl
  dnl PHP_SUBST(MY_MODULE_SHARED_LIBADD)

  PHP_NEW_EXTENSION(my_module, my_module.c, $ext_shared)
fi

もし M4 ファイルにあまりなじみがないのなら (この機会に覚えてしまいましょう)、最初はこの例が難しく感じられるかもしれません。 しかし、実際はこれはとても簡単なものです。

注意: dnl で始まる行はすべてコメントであり、パースされません。

config.m4 ファイルの役割は、 configure に渡されたコマンドラインオプションを パースすることです。つまり、 必要な外部ファイルを読み込んで同じような設定タスクを行わなければならないということです。

デフォルトのファイルは、 configure スクリプトのオプションとして 2 つの設定ディレクティブ --with-my_module および --enable-my_module を作成します。 外部のファイルを参照している場合 (例えば --with-apache ディレクティブが Apache のディレクトリを参照しているように) は最初のオプションを使用します。 拡張モジュールを有効にするかどうかを指定させるだけの場合には 2 番目のオプションを使用します。どちらを使用するかを決めたら、 使用しないほうを削除しなければなりません。つまり、 もし --enable-my_module を使用するのなら --with-my_module のサポートを削除しなければなりません。 逆もまた同様です。

デフォルトでは、ext_skel が作成した config.m4 ファイルは 両方のディレクティブを受けつけ、自動的に拡張モジュールを有効にします。 拡張モジュールを有効にする作業は、PHP_EXTENSION マクロで行われます。ユーザが (--enable-my_module あるいは --with-my_module を明示的に指定して)、 モジュールを組み込むように指示した場合にのみ PHP バイナリにモジュールを組み込むように変更するには、 $PHP_MY_MODULE のチェックを == "yes" に変更します。

if test "$PHP_MY_MODULE" == "yes"; then dnl
    Action.. PHP_EXTENSION(my_module, $ext_shared)
    fi
こうすると、PHP を再コンパイルするたびに --enable-my_module を使用しなければならなくなります。

注意: config.m4 を変更した後は、常に buildconf を実行してください!

設定スクリプトで使用可能な M4 マクロについては、後で詳細に説明します。 この段階では、デフォルトのファイルを使用することにします。

拡張モジュールの作成

まず最初に、非常に単純な拡張モジュールを作成してみましょう。この拡張モジュールは、 パラメータとして受け取った整数値をそのまま返すというだけの関数を実装しています。 ソースを Zend API: PHP のコアをハックする に示します。

例2 単純な拡張モジュール

/* 標準ヘッダを include します */
#include "php.h"

/* エクスポートする関数を宣言します */
ZEND_FUNCTION(first_module);

/* Zend がこのモジュールの内容を知るための、関数リスト */
zend_function_entry firstmod_functions[] =
{
    ZEND_FE(first_module, NULL)
    {NULL, NULL, NULL}
};

/* モジュールについての情報 */
zend_module_entry firstmod_module_entry =
{
    STANDARD_MODULE_HEADER,
    "First Module",
    firstmod_functions,
    NULL, 
    NULL, 
    NULL, 
    NULL, 
    NULL,
    NO_VERSION_YET,
    STANDARD_MODULE_PROPERTIES
};

/* Zend にこのモジュールについて説明するための、標準「スタブ」ルーチンを実装します */
#if COMPILE_DL_FIRST_MODULE
ZEND_GET_MODULE(firstmod)
#endif

/* PHP で使用するための関数を実装します */
ZEND_FUNCTION(first_module)
{
    long parameter;

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

    RETURN_LONG(parameter);
}

これは、PHP モジュールとして動作する完全なコードです。 ソースコードについてはあとですぐに説明しますが、 その前にまずビルド方法について説明しておきましょう (API についての議論の前に、とりあえず実際に動かしてみたいという 短気な人たちのためです)。

注意: この例のソースは、PHP 4.1.0 以降に含まれる Zend の新機能を使用しています。 PHP 4.0.x ではコンパイルできません。

モジュールのコンパイル

モジュールをコンパイルするには、基本的に 2 種類の方法があります。

  • ext ディレクトリ内で、 提供されている "make" 機構を使用します。 これは、動的ロードモジュールを作成することも可能です。

  • ソースを手動でコンパイルします。

最初の方法のほうが断然お勧めです。PHP 4.0 以降、 この仕組みは標準化され、洗練されたビルド手順に組み込まれています。 あまりに洗練されすぎているという事実は、裏を返せば欠点でもあります。 つまり、最初のうちはなかなか理解できないということです。 この章の後半ではより詳細に説明しますが、 まず最初はデフォルトのファイルを使用して進めていきましょう。

2 番目の方法は、(何らかの理由で) PHP の完全なソースツリーを もっていない、すべてのファイルに対するアクセス権限がない、 あるいは単にキーボードと戯れていたいなどといった人たちにお勧めです。 これらはどれもめったにないケースではありますが、 この文書を完全なものにするためにこちらの方法についても説明しておきます。

Make を使用したコンパイル

サンプルソースを標準的な仕組みでコンパイルするには、 PHP ソースツリーの ext ディレクトリ以下に サンプルのディレクトリをコピーします。 それから buildconf を実行すると、 新しい拡張モジュール用の適切なオプションを含む configure スクリプトが作成されます。 デフォルトでは、すべてのサンプルソースは無効になります。 そのため、この作業でビルド手順が破壊されてしまう恐れはありません。

buildconf を実行すると、configure --help の結果に以下が追加されます。

  --enable-array_experiments   BOOK: Enables array experiments
  --enable-call_userland       BOOK: Enables userland module
  --enable-cross_conversion    BOOK: Enables cross-conversion module
  --enable-first_module        BOOK: Enables first module
  --enable-infoprint           BOOK: Enables infoprint module
  --enable-reference_test      BOOK: Enables reference test module
  --enable-resource_test       BOOK: Enables resource test module
  --enable-variable_creation   BOOK: Enables variable-creation module

先ほど Zend API: PHP のコアをハックする で示したモジュールは、 --enable-first_module あるいは --enable-first_module=yes で使用可能になります。

手動でのコンパイル

モジュールを手動でコンパイルするには、以下のコマンドを実行しなければなりません。

作業 コマンド
コンパイル cc -fpic -DCOMPILE_DL_FIRST_MODULE=1 -I/usr/local/include -I. -I.. -I../Zend -c -o <your_object_file> <your_c_file>
リンク cc -shared -L/usr/local/lib -rdynamic -o <your_module_file> <your_object_file(s)>
モジュールをコンパイルするためのコマンドでは、コンパイラに対して 位置独立なコードを出力するように指示しています (-fpic は省略できません)。 また、定数 COMPILE_DL_FIRST_MODULE を指定することで、 モジュールのコードを動的ロード可能なモジュールとしてコンパイルするようにしています (上のテストモジュールではこれをチェックしています。これについては後で説明します)。 これらのオプションに続いて、ソースファイルをコンパイルするために必要な 最小限の標準インクルードパスを指定しています。

注意: サンプルのインクルードパスは、すべて ext ディレクトリからの相対パスです。 別の場所でコンパイルする場合は、それに応じてパスを変更してください。 必要なのは、PHP ディレクトリおよび Zend ディレクトリ、そして (必要に応じて) モジュールの存在するディレクトリです。

リンク用のコマンドも、動的モジュールとしてリンクするためのごく平凡なものです。

コンパイル用のコマンドに最適化オプションを含めることもできますが、 この例では省略しています (しかし、いくつかのオプションは 先に説明した makefile のテンプレートに含められています)。

注意: 手動でのコンパイルおよびリンクで PHP に静的モジュールとして組み込む手順は、非常に面倒なものとなります。 そのため、ここでは扱いません (これらのコマンドをすべてタイプするのは効率的ではありません)。

拡張モジュールの使用

選択したビルド方式によって、新しい PHP バイナリを Web サーバにリンクする (あるいは CGI として実行する) か .so (共有オブジェクト) ファイルを 作成するかのいずれかとなります。ファイル first_module.c を共有オブジェクトとしてコンパイルしたのなら、結果として first_module.so が出来上がります。これを 使用するために最初にしなければならないことは、出来上がったファイルを PHP からアクセス可能な位置に配置することです。とりあえず試してみるなら、 そのファイルを htdocs ディレクトリにコピーして Zend API: PHP のコアをハックする のソースで試してみましょう。 PHP バイナリに組み込む形式でコンパイルした場合は dl() のコールを省略します。そのモジュールの 機能はスクリプト内からすぐに利用可能です。

警告

セキュリティを確保するため、動的モジュールを公開ディレクトリに 配置してはいけません。公開ディレクトリに 配置することもできなくはないですし それによってテストも簡単にできますが、実運用環境では 別のディレクトリに配置すべきです。

例3 first_module.so をテストするためのファイル

<?php
    
// 必要に応じて次の行のコメントを解除します
// dl("first_module.so"); 

$param 2;
$return first_module($param);

print(
"'$param' を送信すると、'$return' が返されました");

?>

この PHP ファイルをコールした結果は、以下のようになります。

'2' を送信すると、'2' が返されました

必要なら、dl() 関数をコールすることによって 動的モジュールを読み込みます。この関数は 指定した共有オブジェクトを探し、それを読み込み、そして その関数を PHP から使用できるようにします。 このモジュールが提供する関数は first_module() で、ひとつのパラメータを受け取ってそれを整数に変換し、 変換結果を返します。

同じ結果が得られましたか? おめでとう! はじめての PHP 拡張のビルドがこれで完了しました。

トラブルシューティング

実際のところ、静的モジュールおよび動的モジュールをコンパイルする際に 行えるトラブルシューティングはそれほど多くありません。発生する 可能性のある唯一の問題は、コンパイラが「……が未定義」のような メッセージを出すことくらいです。このような場合は、 すべてのヘッダファイルが存在し、コンパイルコマンドでそれらへのパスを 正しく設定しているかどうかを確認しましょう。 すべてが正しく配置されていることを確実にするには、PHP ソースツリーを 新しく展開しなおし、ext ディレクトリ内に ファイルをコピーして自動ビルドを使用します。これにより、 安全なコンパイル環境が保証されます。これが失敗した場合には 手動でのコンパイルを試みます。

モジュール内で関数が見つからないというメッセージを PHP から受け取る こともあるかもしれません (もしサンプルソースを変更せずにそのまま 使用したのなら、これは起こりえません)。モジュール内から アクセスしようとしている外部関数にスペルミスがあれば、それは シンボルテーブルに "未リンクのシンボル" として残るでしょう。 PHP から動的に読み込んでリンクされる際に、これらは型エラーの ために読み込みに失敗します。本体のバイナリに対応するシンボルが 存在しないからです。このような場合は、モジュールファイルの中から 間違った宣言や外部参照を見つけ出します。この問題は、動的モジュール 固有のものであることに注意しましょう。静的モジュールとして 作成した場合は、この問題はコンパイル時に発覚します。

ソースコードについての議論

さあ、今やあなたは安全なビルド環境を手にいれ、 モジュールを PHP に組み込めるようになりました。 そろそろ全体像について話を始める時期でしょう。

モジュールの構造

すべての PHP モジュールは、共通の構造に従っています。

  • ヘッダファイルのインクルード (必要なすべてのマクロ、API、define などをインクルードするため)

  • エクスポートする関数の C での宣言 (Zend 関数ブロックの宣言のために必要)

  • Zend 関数ブロックの宣言

  • Zend モジュールブロックの宣言

  • get_module() の実装

  • エクスポートするすべての関数の実装

ヘッダファイルのインクルード

モジュールに組み込まなければならない唯一のヘッダファイルは php.h で、これは PHP ディレクトリにあります。 このファイルは、新しいモジュールを使用できるようにビルドするための すべてのマクロ定義、API 定義を含みます。

豆知識: モジュール固有の定義を含むヘッダファイルを、 別に分けて作成しておくとよいでしょう。 エクスポートされるすべての関数の定義をこのヘッダファイルに含め、 そしてこのファイルで php.h をインクルードします。 ext_skel を使用して雛形を作成したのなら、 すでにこの形式のヘッダファイルが出来上がっているはずです。

エクスポートする関数の宣言

エクスポートする (つまり PHP の新しいネイティブ関数として使用できるようにする) 関数を宣言するためには、Zend が提供するマクロを使用します。 宣言は、このように行います。

ZEND_FUNCTION ( my_function );

ZEND_FUNCTION は、Zend の内部 API を満たす新しい C 関数を宣言します。内部 API を満たすとは、 その関数の型が void であり、パラメータとして INTERNAL_FUNCTION_PARAMETERS (別のマクロ) を受け取るということです。さらに、関数名の先頭に zif を付加します。これらの定義を満たす宣言の例は、次のようになります。

void zif_my_function ( INTERNAL_FUNCTION_PARAMETERS );
INTERNAL_FUNCTION_PARAMETERS を展開した結果は、次のようになります。
void zif_my_function( int ht
                    , zval * return_value
                    , zval * this_ptr
                    , int return_value_used
                    , zend_executor_globals * executor_globals
                    );

インタプリタおよびエグゼキュータのコアは PHP 本体のパッケージとは分離されているので、 マクロや関数群を定義するもうひとつの API が作られました。これが Zend API です。今のところ Zend API は、 かつて PHP が担当していた機能のうちのごくわずかしか処理できません。 PHP の関数の多くは Zend のマクロに書き下ろされており、 その内部で Zend API をコールしています。 おすすめのやりかたは、まずできる限り Zend API を使用することです。 というのは古い API は互換性のためだけに残されているものだからです。 例えば、zval 型と pval 型はまったく同じものです。 zval は Zend での定義で、pval は PHP での定義です (実際のところ、現在 pvalzval のエイリアスとなっています)。 INTERNAL_FUNCTION_PARAMETERS は Zend のマクロなので、 上の宣言には zval が用いられています。 コードを書く際には、新しい Zend API のために常に zval を使うようにしましょう。

この宣言のパラメータリストは非常に重要です。これらは常に頭に入れておくようにしましょう。 (Zend API: PHP のコアをハックする を参照ください)。

PHP からコールされた際に関数に渡される Zend のパラメータ
パラメータ 説明
ht Zend の関数に渡す引数の数。これを直接操作してはいけません。値を取得する際には ZEND_NUM_ARGS() を使用してください。
return_value この変数は、あなたの関数から PHP に返す値を渡すために使用します。 この変数にアクセスするには、定義済みマクロを使用するのがベストです。 マクロについては後で説明します。
this_ptr この変数を使用すると、関数がオブジェクト内で使用されている場合に そのオブジェクトにアクセスすることができます。このポインタを取得するには 関数 getThis() を使用します。
return_value_used このフラグは、この関数の返す値が実際に呼び出し元のスクリプトで使われるのかどうかを指定します。 0 は、返り値が使われないことを表します。 1 は、呼び出し元が返り値を期待していることを表します。 このフラグの値により、関数が正しく使用されているかどうかを確認します。また、 値を返す処理に負荷がかかる場合などに、このフラグによって速度の最適化を行います (array.c で、このような使用法を用いています)。
executor_globals この変数は、Zend エンジンのグローバル設定へのポインタです。 これが便利なのは、例えば新しい変数を作成するときです (詳細はあとで説明します)。 エグゼキュータのグローバル設定は、マクロ TSRMLS_FETCH() を使うことによってもあなたの関数で使用できます。

Zend 関数ブロックの宣言

エクスポートする関数の宣言が完了しました。こんどはさらにそれを Zend に登録しなければなりません。関数のリストを登録するには、 zend_function_entry の配列を使用します。 この配列には、外部に公開するすべての関数について PHP で使用する場合の名前と C ソース内で定義されている名前が含まれています。 内部的には、zend_function_entryZend API: PHP のコアをハックする のように定義されています。

例4 内部での zend_function_entry の宣言

typedef struct _zend_function_entry {
    char *fname;
    void (*handler)(INTERNAL_FUNCTION_PARAMETERS);
    unsigned char *func_arg_types;
} zend_function_entry;
エントリ 説明
fname PHP で使用する場合の名前を表します (例えば fopenmysql_connect など。 今回の例では first_module)。
handler この関数コールを処理する C 関数へのポインタ。 例えば、先ほど説明した標準マクロ INTERNAL_FUNCTION_PARAMETERS を参照ください。
func_arg_types パラメータに対して、参照渡しを強制させるようにできます。 通常は NULL を設定しておくべきです。
上の例では、このような宣言になります。
zend_function_entry firstmod_functions[] =
{
    ZEND_FE(first_module, NULL)
    {NULL, NULL, NULL}
};
リストの最後のエントリは、必ず {NULL, NULL, NULL} でなければなりません。 エクスポートされる関数の一覧が終わることを Zend が知るために、 このエントリが必要となります。

注意: 最後を表す印として、定義済みマクロを使用することは できません。そうすると、 "NULL" という名前の関数を探しにいってしまいます!

マクロ ZEND_FE ('Zend Function Entry' を省略したものです) は、構造体のエントリを単純に zend_function_entry に展開します。 これらのマクロは特別な命名規約を持っていることに注意しましょう。 あなたが作成する C 関数の名前には、前に zif_ をつけることになります。つまり、ZEND_FE(first_module)zif_first_module() という名前の C 関数を参照するということです。このマクロと手書きのエントリを混ぜて使用する場合には (お勧めしません) これを頭に入れておきましょう。

豆知識: zif_*() という名前の関数でコンパイルエラーが出た場合は、 ZEND_FE で定義した関数がかかわっています。

Zend API: PHP のコアをハックする が、 関数の定義に使用できるすべてのマクロの一覧です。

関数定義用のマクロ
マクロ名 説明
ZEND_FE(name, arg_types) zend_function_entryname という名前の関数エントリを定義します。対応する C の関数が必要です。 arg_typesNULL に設定しなければなりません。 この関数は、自動的に C の関数名を生成します。その名前は PHP の関数名の先頭に zif_ をつけたものになります。 例えば ZEND_FE("first_module", NULL) とすると PHP の関数 first_module() を登録したことになり、 それを C の関数 zif_first_module() と関連付けます。 ZEND_FUNCTION と組み合わせて使用します。
ZEND_NAMED_FE(php_name, name, arg_types) php_name という名前で PHP の関数を定義し、 それを対応する C の関数 name に関連付けます。 arg_typesNULL に設定しなければなりません。ZEND_FE のように自動的に名前を決められたくない場合にこの関数を使用します。 ZEND_NAMED_FUNCTION と組み合わせて使用します。
ZEND_FALIAS(name, alias, arg_types) alias という名前で、 name のエイリアスを定義します。 arg_typesNULL に設定しなければなりません。対応する C の関数は不要です。 その代わりに、エイリアスの対象を参照します。
PHP_FE(name, arg_types) 古い PHP の API で、ZEND_FE と同じものです。
PHP_NAMED_FE(runtime_name, name, arg_types) 古い PHP の API で、ZEND_NAMED_FE と同じものです。

注意: ZEND_FEPHP_FUNCTION を組み合わせて使用したり、あるいは PHP_FEZEND_FUNCTION を組み合わせて使用したりすることはできません。しかし、 各関数について ZEND_FEZEND_FUNCTION、あるいは PHP_FEPHP_FUNCTION という組み合わせが守られているのなら、 それらを混用することは可能です。 しかし、混用することは 推奨しませんZEND_* マクロだけを使用するようにしましょう。

Zend モジュールブロックの宣言

このブロックは構造体 zend_module_entry に保存され、モジュールの内容について Zend に示すために必要な すべての情報が含まれます。このモジュールの内部定義は Zend API: PHP のコアをハックする で確認できます。

例5 内部での zend_module_entry の宣言

typedef struct _zend_module_entry zend_module_entry;
     
    struct _zend_module_entry {
    unsigned short size;
    unsigned int zend_api;
    unsigned char zend_debug;
    unsigned char zts;
    char *name;
    zend_function_entry *functions;
    int (*module_startup_func)(INIT_FUNC_ARGS);
    int (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS);
    int (*request_startup_func)(INIT_FUNC_ARGS);
    int (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS);
    void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS);
    char *version;

[ 構造体の残りの部分は、ここではあまり関係がありません ]

};
エントリ 説明
size, zend_api, zend_debug および zts 通常は "STANDARD_MODULE_HEADER" を指定します。 これは、4 つのメンバにそれぞれ zend_module_entory 全体のサイズ、ZEND_MODULE_API_NO、 デバッグビルドか通常ビルドのどちらであるか (ZEND_DEBUG) そして ZTS が有効かどうか (USING_ZTS) を代入します。
name モジュール名を指定します (例えば "File functions""Socket functions""Crypt" など)。この名前は、 phpinfo() の "Additional Modules" 欄で使用されます。
functions 先ほど説明した Zend 関数ブロックへのポインタ。
module_startup_func この関数はモジュールの初期化時にコールされ、 最初の一度だけ行う初期化処理 (例えばメモリの確保など) で使用します。初期化に失敗した場合には FAILURE、 成功した場合には SUCCESS を返します。 このフィールドを使用しない場合は、NULL を指定します。関数を宣言するには、マクロ ZEND_MINIT を使用します。
module_shutdown_func この関数はモジュールのシャットダウン時にコールされ、 最後に一度だけ行う後処理 (例えばメモリの開放など) で使用します。 これは module_startup_func() に対応するものです。 で使用します。後処理に失敗した場合には FAILURE、 成功した場合には SUCCESS を返します。 このフィールドを使用しない場合は、NULL を指定します。関数を宣言するには、マクロ ZEND_MSHUTDOWN を使用します。
request_startup_func この関数はページがリクエストされるたびにコールされ、 リクエストを処理する際の前処理で使用します。 処理に失敗した場合には FAILURE、 成功した場合には SUCCESS を返します。 注意: 動的モジュールの場合は リクエストがあるまでは読み込まれないので、 リクエストスタートアップ関数は、 モジュールスタートアップ関数の直後にコールされます (これら二つの初期化イベントが同時に発生します)。 このフィールドを使用しない場合は、NULL を指定します。関数を宣言するには、マクロ ZEND_RINIT を使用します。
request_shutdown_func この関数はページのリクエストが終了するたびにコールされます。 ちょうど request_startup_func() に対応するものです。 処理に失敗した場合には FAILURE、 成功した場合には SUCCESS を返します。 注意: 動的モジュールの場合は リクエストがあるまでは読み込まれないので、 リクエストシャットダウン関数の直後に モジュールシャットダウンハンドラがコールされます (これら二つの後処理イベントが同時に発生します)。 このフィールドを使用しない場合は、NULL を指定します。関数を宣言するには、マクロ ZEND_RSHUTDOWN を使用します。
info_func スクリプト内で phpinfo() がコールされると、 Zend は現在読み込まれているすべてのモジュールについてこの関数をコールします。 つまり、その出力結果に何らかの "あしあと" を残すチャンスが すべてのモジュールに与えられるわけです。一般的には、 これを使用して環境情報や東経情報を出力します。 このフィールドを使用しない場合は、NULL を指定します。関数を宣言するには、マクロ ZEND_MINFO を使用します。
version モジュールのバージョン。バージョン番号をまだつけたくない場合は NO_VERSION_YET が使用できます。しかし、 何らかのバージョン文字列を指定することを推奨します。 バージョン文字列は、例えば次のようなものになります (バージョンの若い順に並べています): "2.5-dev""2.5RC1""2.5" あるいは "2.5pl3"
それ以外の構造体の要素 これらは内部的に使用されるもので、マクロ STANDARD_MODULE_PROPERTIES_EX を使用して事前に設定されます。これらの要素に値を代入してはいけません。 STANDARD_MODULE_PROPERTIES_EX を使用するのは、 グローバルなスタートアップ関数、シャットダウン関数を使用する場合のみです。 それ以外の場合は STANDARD_MODULE_PROPERTIES を直接使用します。

今回の例では、この構造体を次のように実装します。

zend_module_entry firstmod_module_entry =
{
    STANDARD_MODULE_HEADER,
    "First Module",
    firstmod_functions,
    NULL, NULL, NULL, NULL, NULL,
    NO_VERSION_YET,
    STANDARD_MODULE_PROPERTIES,
};
これは、基本的に必要最小限の設定です。モジュール名は First Module とし、関数一覧の参照を設定し、 スタートアップ関数やシャットダウン関数はすべて未使用としています。

参照用として、スタートアップ関数およびシャットダウン関数に関するマクロを Zend API: PHP のコアをハックする にまとめておきます。 これらは今回の例では使用しませんが、後で説明します。 スタートアップ関数やシャットダウン関数を宣言する際にはこれらのマクロを使用すべきです。 というのもこれらの関数には特別なパラメータ (INIT_FUNC_ARGS および SHUTDOWN_FUNC_ARGS) を渡さなければならず、 定義済みマクロを使用することでこれらが自動的に関数宣言に組み込まれるからです。 仮にこれらの関数を (マクロを使用せずに) 手動で宣言したとしましょう。 もし PHP の開発者が何らかの事情で引数を変更したとすると、 それに追従するためにあなたは自分のモジュールのソースを変更しなければならなくなります。

スタートアップ関数、シャットダウン関数を宣言するためのマクロ
マクロ 説明
ZEND_MINIT(module) モジュールの開始時の関数を宣言します。 関数名は zend_minit_<module> (例えば zend_minit_first_module) のようになります。 ZEND_MINIT_FUNCTION と組み合わせて使用します。
ZEND_MSHUTDOWN(module) モジュールのシャットダウン時の関数を宣言します。 関数名は zend_mshutdown_<module> (例えば zend_mshutdown_first_module) のようになります。 ZEND_MSHUTDOWN_FUNCTION と組み合わせて使用します。
ZEND_RINIT(module) リクエストの開始時の関数を宣言します。 関数名は zend_rinit_<module> (例えば zend_rinit_first_module) のようになります。 ZEND_RINIT_FUNCTION と組み合わせて使用します。
ZEND_RSHUTDOWN(module) リクエストのシャットダウン時の関数を宣言します。 関数名は zend_rshutdown_<module> (例えば zend_rshutdown_first_module) のようになります。 ZEND_RSHUTDOWN_FUNCTION と組み合わせて使用します。
ZEND_MINFO(module) モジュール情報を出力する関数を宣言します。 phpinfo() がコールされた際に使用されます。 関数名は zend_info_<module> (例えば zend_info_first_module) のようになります。 ZEND_MINFO_FUNCTION と組み合わせて使用します。

get_module() の作成

これは特別な関数で、すべての動的読み込みモジュールで使用されます。 これを作成するため、まずは ZEND_GET_MODULE を見てみましょう。

#if COMPILE_DL_FIRSTMOD
     ZEND_GET_MODULE(firstmod) 
#endif

関数の実装が、条件付きコンパイル文で囲まれています。なぜかというと、 get_module() 関数が必要となるのは あなたのモジュールが動的モジュールとしてビルドされる場合だけだからです。 コンパイラのコマンドで COMPILE_DL_FIRSTMOD を定義することにより (動的モジュールとしてビルドするための コンパイル手順については上記を参照ください)、 動的モジュールとしてビルドするのか組み込みモジュールとしてビルドするのかを 指示することができます。組み込みモジュールを作成する場合は、 get_module() の実装は単純に取り除かれます。

get_module() は、 Zend がモジュールを読み込む際にコールされます。 例えば、スクリプト内で dl() がコールされた場合などです。この関数の目的は、モジュール情報ブロックを Zend に返し、モジュールの内容をエンジンに教えることです。

動的モジュールで get_module() 関数を実装しなかった場合、 Zend がそのモジュールにアクセスしようとした際にエラーとなります。

エクスポートするすべての関数の実装

あとは、エクスポートする関数を実装すれば終わりです。 first_module では、例としてこのような関数を使用します。

ZEND_FUNCTION(first_module)
{
    long parameter;

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

    RETURN_LONG(parameter);
}
関数の宣言には ZEND_FUNCTION を使用します。 これは、関数エントリテーブルにおける ZEND_FE に対応しています (こちらについては先ほど説明しました)。

関数宣言の後には、引数のチェックおよびその内容の取得、 引数の変換、そして返り値の作成などのコードが続きます (これらの詳細は後述します)。

まとめ

基本的に、これですべてです。PHP モジュールを構成するのに、 これ以上のものは必要ありません。組み込みのモジュールについても その構造は動的モジュールと同じです。 ここまでに説明した内容を身に着けておけば、今後 PHP モジュールのソースファイルを読んでいく際につまづくこともなくなるでしょう。

さあ、これ以降の節で PHP の内部構造について学び、 強力な拡張モジュールを作っていきましょう。

引数の扱い

言語を拡張する際の最も重要な問題のひとつは、 引数として渡されるデータの扱いです。たいていの拡張モジュールは、 何らかの入力データを扱う (あるいはパラメータを受け取って 特定の動作を行う) ように作られています。そして、 PHP と C 言語の間でデータをやり取りするための唯一の方法が 関数の引数となります。 事前に定義したグローバル値を使用してデータを交換することももちろん可能ですが (この方法についても後述します)、いろんな意味でこの方法は避けるべきです。 これはまったくお勧めできない手段です。

PHP では、正式に関数を宣言 (declare) することはありません。 そのため、関数コールの構文は完全に動的なものとなり、 エラーチェックは行われません。 コール方法が正しいかどうかを調べるのは、ユーザが書くコードの役割となります。 例えば、ある関数に対して引数をひとつだけ指定してコールした後、 同じ関数に対して 4 つの引数を指定してコールすることも可能です。 どちらのコールについても、文法的にはまったく正しいものとなります。

引数の数の定義

PHP が正式な関数宣言を行わないためにコール時の文法チェックが行われない、 また PHP が可変引数をサポートしているなどの理由で、 実際にその関数にいくつの引数が渡されたのかを知らなければならないこともあるでしょう。 そのような場合には ZEND_NUM_ARGS マクロを使用します。 以前のバージョンの PHP では、コール時の引数の数を取得するために このマクロは関数のハッシュテーブルのエントリ ht を使用していました。このエントリは INTERNAL_FUNCTION_PARAMETERS のリストから渡されました。 関数に渡された引数の数は今では ht 自体に含まれているので、ZEND_NUM_ARGS はダミーのマクロとなっています (実際の定義は zend_API.h を参照ください)。 しかし、今後のことを考えると、 呼び出しインターフェイスが変わっても互換性を保ち続けられるように このマクロを使用しておくことをお勧めします。 注意: 昔の PHP では、このマクロと同等の働きをするのは ARG_COUNT マクロでした。

引数の数が正しいかどうかを調べるコードは次のようになります。

if(ZEND_NUM_ARGS() != 2) WRONG_PARAM_COUNT;
    
関数コール時の引数の数が 2 つでなかった場合、 これはエラーメッセージを表示して終了します。 上のコードは、WRONG_PARAM_COUNT マクロの使用例でもあります。 これは、以下のような標準的なエラーメッセージを生成するために使用するものです。
"Warning: Wrong parameter count for firstmodule() in /home/www/htdocs/firstmod.php on line 5"

このマクロはデフォルトのエラーメッセージを表示し、呼び出し元に制御を戻します。 このマクロの定義もまた zend_API.h にあります。このような内容です。

ZEND_API void wrong_param_count(void);

#define WRONG_PARAM_COUNT { wrong_param_count(); return; }
ご覧の通り、このマクロは wrong_param_count() という名前の内部関数をコールしています。 この関数が警告を表示しています。独自のエラーメッセージを作成するための方法については、 後半の節「情報の表示」を参照ください。

引数の取得

注意: パラメータのパース用の新しい API
この章では、Andrei Zmievski による新しい Zend パラメータパース用 API を説明します。この API は PHP 4.0.6 から PHP 4.1.0 の間の開発中に導入されました。

パラメータのパースはあまりにもありふれた操作であり、少し退屈に感じることもあるでしょう。 また、エラーチェックやエラーメッセージは標準化されていたほうがいいでしょう。 PHP 4.1.0 以降では、パラメータのパース用の新しい API を使用することでこれが実現できます。 この API はパラメータの受け取りを劇的に単純化していますが、 可変引数を受け取る関数には使用できないという弱点があります。 しかし、大半の関数では、引数の数は固定です。そのため、 新しい標準として、このパース用 API の使用を推奨します。

パラメータのパース用関数のプロトタイプは、このようになります。

int zend_parse_parameters(int num_args TSRMLS_DC, char *type_spec, ...);
最初の引数に、あなたの関数が受け取るパラメータの数を指定します。 ここでは ZEND_NUM_ARGS() が使用できます。 2 番目のパラメータは、常に TSRMLS_CC マクロでなければなりません。3 番目の引数は、 あなたの関数が受け取る引数の数および型を表す文字列です。 これは、printf の書式指定文字列によって出力内容を指定するのに似ています。 そして最後に、パラメータの値を受け取る変数へのポインタを指定します。

常に希望通りの型でデータを受け取ることができるよう、 zend_parse_parameters() は可能な範囲で型変換を行います。 あらゆるスカラー型は別のスカラー方に変換することが可能です。 しかし、複雑な型 (配列、オブジェクトあるいはリソース) とスカラー型の間の型変換はできません。

パラメータの取得に成功し、かつ型変換でエラーが発生しなかった場合は この関数は SUCCESS を返します。それ以外の場合は FAILURE を返します。また、 「パラメータの数が一致しない」「型変換ができなかった」 などの情報を含むエラーメッセージを出力します。

出力されるエラーメッセージは、例えば次のようなものになります。

     Warning - ini_get_all() requires at most 1 parameter, 2 given
     Warning - wddx_deserialize() expects parameter 1 to be string, array given
    
もちろん、これらのメッセージにはエラーの発生したファイル名や行番号も含まれています。

型を指定する文字の一覧をここにまとめます。

  • l - long

  • d - double

  • s - string (null バイトの可能性もあり) およびその長さ

  • b - boolean

  • r - zval* に保存されたリソース

  • a - zval* に保存された配列

  • o - zval* に保存された (あらゆるクラスの) オブジェクト

  • O - zval* に保存された (クラスエントリで指定されているクラスの) オブジェクト

  • z - zval* 自体

以下の文字は、型指定文字列の中で特別な意味を持ちます。
  • | - 残りのパラメータがオプションであることを表します。 これらのパラメータに対応する保存用変数は、 拡張モジュール自身によってデフォルト値で初期化されなければなりません。 なぜなら、パラメータが渡されていなければパース関数を通過しないからです。

  • / - パラメータの後にこの文字を続けると、 そのパラメータに対して SEPARATE_ZVAL_IF_NOT_REF() をコールします。これにより、そのパラメータが参照でなければパラメータのコピーが作成されます。

  • ! - パラメータの後にこの文字を続けると、 そのパラメータは指定した型あるいは NULL となります (a、o、O、r および z についてのみ適用可能)。 NULL 値が渡された場合は、 保存ポインタの値が NULL に設定されます。

この関数の使用法について説明するには、 実際の例を見ていただくのがいちばんでしょう。

/* long、文字列とその長さ、そして zval を受け取ります。*/
long l;
char *s;
int s_len;
zval *param;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC,
                          "lsz", &l, &s, &s_len, &param) == FAILURE) {
    return;
}

/* my_ce で指定するクラスのオブジェクト、そしてオプションで double 値を受け取ります。*/
zval *obj;
double d = 0.5;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC,
                          "O|d", &obj, my_ce, &d) == FAILURE) {
    return;
}

/* オブジェクト (あるいは null)、そして配列を受け取ります。
   オブジェクトに null が渡された場合、obj は NULL となります。*/
zval *obj;
zval *arr;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "O!a", &obj, &arr) == FAILURE) {
    return;
}

/* 配列のコピーを受け取ります。*/
zval *arr;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "a/", &arr) == FAILURE) {
    return;
}

/* 最初の 3 つのパラメータのみを受け取ります (可変引数の関数の場合に有用です)。*/
zval *z;
zend_bool b;
zval *r;
if (zend_parse_parameters(3, "zbr!", &z, &b, &r) == FAILURE) {
    return;
}

最後の例で、ZEND_NUM_ARGS() を使用せずにパラメータ数を 3 としていることに注目しましょう。 こうすることで、可変引数の関数に対して最低限必要な引数の数を指定することができます。 もちろん、もし残りのパラメータについても処理したいのなら zend_get_parameters_array_ex() を使用してそれを取得しなければなりません。

拡張版のパース関数も存在します。 これは、追加のフラグを引数に指定することでその動きを制御します。

int zend_parse_parameters_ex(int flags, int num_args TSRMLS_DC, char *type_spec, ...);

現在は、渡すことのできるフラグは ZEND_PARSE_PARAMS_QUIET だけです。これを指定すると、処理中に発生したエラーメッセージを出力しないようになります。 これは、その関数がさまざまな形式の引数を受け取ることを想定しており、 独自のエラーメッセージで対応したい場合などに有用です。

例えば、3 つの long 値あるいは 3 つの文字列を受け取る関数は、 このようになります。

long l1, l2, l3;
char *s;
if (zend_parse_parameters_ex(ZEND_PARSE_PARAMS_QUIET,
                             ZEND_NUM_ARGS() TSRMLS_CC,
                             "lll", &l1, &l2, &l3) == SUCCESS) {
    /* long の場合 */
} else if (zend_parse_parameters_ex(ZEND_PARSE_PARAMS_QUIET,
                                    ZEND_NUM_ARGS(), "s", &s, &s_len) == SUCCESS) {
    /* 文字列の場合 */
} else {
    php_error(E_WARNING, "%s() takes either three long values or a string as argument",
              get_active_function_name(TSRMLS_C));
    return;
}

上で説明したいずれの場合についても、パラメータの取得処理は慎重に行いましょう。 これ以外の例については、PHP に同梱されている拡張モジュールのソースを 参考にしてください。便利な使用法がいろいろ発見できるでしょう。

引数の取得の古い方法 (廃止予定)

注意: 廃止予定のパラメータパース用 API
この API は非推奨です。現在は新しい ZEND パラメータパース API が使用されています。

引数の数をチェックし終えたら、次は引数そのものにアクセスしなければなりません。 そのためには zend_get_parameters_ex() の助けを借りることになります。

zval **parameter;

if(zend_get_parameters_ex(1, &parameter) != SUCCESS)
  WRONG_PARAM_COUNT;
すべての引数は zval コンテナに格納され、 二重にポイントされる必要があります。 上のコードは、引数を取得してそれをポインタ parameter でアクセスできるようにしています。

zend_get_parameters_ex() は、少なくとも 2 つの引数を受け付けます。 最初の引数は、取得する引数の数です (これは、 関数がコールされた際の引数の数と同じでなければなりません。 そのため、呼び出し構文をきちんとチェックすることが大切になります)。 2 番目 (およびそれ以降) の引数は、zval へのポインタへのポインタへのポインタ (なんてややこしいことでしょう!) です。 これが必要になるのは、私たちが作成した関数内のローカル **zval を管理するために Zend は内部で **zval を使用しており、 zend_get_parameters_ex() はそれに対するポインタを必要とするからです。

zend_get_parameters_ex() の返り値は SUCCESS あるいは FAILURE で、それぞれ (当然のごとく) 成功したこと、あるいは引数の処理に失敗したことを示します。 もっともありがちな失敗の原因は、パラメータの数を間違えることです。 この場合は、 WRONG_PARAM_COUNT で関数を抜けなければなりません。

複数の引数を受け取るには、このようにします。

zval **param1, **param2, **param3, **param4;
     
if(zend_get_parameters_ex(4, &param1, &param2, &param3, &param4) != SUCCESS)
    WRONG_PARAM_COUNT;

zend_get_parameters_ex() がエラーとなるのは、 実際の数より多くのパラメータを取得しようとした場合のみです。 もし実際の関数が 5 つの引数でコールされたのに zend_get_parameters_ex() では 3 つしか取得しなかった場合、 何もエラーは発生せず、最初の 3 つのパラメータが取得されます。 続けて zend_get_parameters_ex() をコールしたとしても、 残りの引数が取得できるわけではなく最初と同じものが得られるだけです。

可変引数 / オプションパラメータの扱い

もしあなたの関数が可変引数を受け付けるのなら、 さきほどの例が次善の策となることもあるでしょう。ただ、 取りうる可能性のあるすべての引数の数に対して、それぞれ zend_get_parameters_ex() コールしなければならず、 不満が残ります。

このような場合には、 zend_get_parameters_array_ex() 関数を使うことができます。 これは、引数の数を指定してデータを取得し、それを配列に格納します。

zval **parameter_array[4];

/* 引数の数を取得します */
argument_count = ZEND_NUM_ARGS();

/* 引数の数の範囲 (最低 2 個、最大 4 個) */
/* を満たしているかどうかを調べます      */
if(argument_count < 2 || argument_count > 4)
    WRONG_PARAM_COUNT;

/* 引数の数が正しかったので、その内容を取得します */
if(zend_get_parameters_array_ex(argument_count, parameter_array) != SUCCESS)
    WRONG_PARAM_COUNT;
まず、引数の数を調べてそれが予期する範囲内にあることを確かめます。 それから zend_get_parameters_array_ex() を使用し、 引数の値へのポインタを parameter_array に格納します。

これを非常にうまく実装したのが、PHP の fsockopen() を処理するコードです。このコードは ext/standard/fsock.c にあり、 Zend API: PHP のコアをハックする で見ることができます。 このソースの中に知らない関数が含まれていたとしても、現時点では問題ありません。 すぐにわかるようになります。

例6 fsockopen() における、PHP の可変引数処理の実装

pval **args[5];
int *sock=emalloc(sizeof(int));
int *sockp;
int arg_count=ARG_COUNT(ht);
int socketd = -1;
unsigned char udp = 0;
struct timeval timeout = { 60, 0 };
unsigned short portno;
unsigned long conv;
char *key = NULL;
FLS_FETCH();

if (arg_count > 5 || arg_count < 2 || zend_get_parameters_array_ex(arg_count,args)==FAILURE) {
    CLOSE_SOCK(1);
    WRONG_PARAM_COUNT;
}

switch(arg_count) {
    case 5:
        convert_to_double_ex(args[4]);
        conv = (unsigned long) (Z_DVAL_PP(args[4]) * 1000000.0);
        timeout.tv_sec = conv / 1000000;
        timeout.tv_usec = conv % 1000000;
        /* fall-through */
    case 4:
        if (!PZVAL_IS_REF(*args[3])) {
            php_error(E_WARNING,"error string argument to fsockopen not passed by reference");
        }
        pval_copy_constructor(*args[3]);
        ZVAL_EMPTY_STRING(*args[3]);
        /* fall-through */
    case 3:
        if (!PZVAL_IS_REF(*args[2])) {
            php_error(E_WARNING,"error argument to fsockopen not passed by reference");
            return;
        }
        ZVAL_LONG(*args[2], 0);
        break;
}

convert_to_string_ex(args[0]);
convert_to_long_ex(args[1]);
portno = (unsigned short) Z_LVAL_P(args[1]);

key = emalloc(Z_STRLEN_P(args[0]) + 10);

fsockopen() が受け付ける引数の数は、 2、3、4、あるいは 5 です。お決まりの変数宣言の後に、 この関数は引数の数が範囲内にあるかどうかを調べます。 それから、switch() 文の下に流れていく性質を利用して、 すべての引数を処理します。switch() 文は、 まず引数の数が最大 (5 個) であった場合の処理から始まります。 その後、引数の数が 4 個であった場合の処理、3 個であった場合の処理、 と順にたどっていきます。これは、キーワード break の記述を省略しているからです。最後の処理を実行した後で switch() 文は終了し、 関数の引数が 2 個だけであった場合でも、必要最小限の処理が実行されます。

このように複数ステージの処理を段階的に実行するようにすると、 可変引数の処理がやりやすくなります。

引数へのアクセス

引数にアクセスするには、すべての引数の型がきちんと定義されていなければなりません。 何度も言いますが、PHP は究極の動的言語なので、時に問題が起こることがあります。 PHP は一切の型チェックを行わないので、 どんな型のデータでも関数に渡すことができてしまいます。 本当は整数値を期待しているのに配列が渡されるかもしれないし、 その逆だってありえます。PHP は、こんな場合でも一切なにも通知しません。

これを防ぐには、これらの API を使用して引数の方を強制的に変換しなければなりません (Zend API: PHP のコアをハックする を参照ください)。

注意: すべての変換関数のパラメータは **zval です。

引数の変換関数
関数 説明
convert_to_boolean_ex() Boolean 型への強制的な変換を行います。 Boolean 値が渡された場合は何もしません。 Long、double および 0 を含む文字列、 そして NULL 値は Boolean 0 (FALSE) となります。配列やオブジェクトは、 その元になっているエントリやプロパティの数を基準にして変換されます。 空の配列や空のオブジェクトは FALSE、それ以外は TRUE となります。 その他の値は、すべて Boolean 1 (TRUE) になります。
convert_to_long_ex() long 型 (デフォルトの整数型) への強制的な変換を行います。 NULL 値、Boolean、リソースおよび long はそのまま何もしません。 double は切り詰められます。整数値を含む文字列は対応する数値表現に変換され、 それ以外の文字列は 0 になります。 配列やオブジェクトは、中身が空の場合に 0、 それ以外の場合に 1 となります。
convert_to_double_ex() double 型 (デフォルトの浮動小数点数値型) への強制的な変換を行います。 NULL 値、Boolean、リソース、long、そしてもちろん double はそのまま何もしません。数値を含む文字列は対応する数値表現に変換され、 それ以外の文字列は 0.0 になります。 配列やオブジェクトは、中身が空の場合に 0.0、 それ以外の場合に 1.0 となります。
convert_to_string_ex() 文字列への強制的な変換を行います。文字列が渡された場合は何もしません。 NULL 値は空の文字列に変換されます。Boolean TRUE は "1"、それ以外の Boolean は空の文字列となります。 long および double はそれぞれ対応する文字列表現に変換されます。 配列は "Array"、オブジェクトは "Object" という文字列に変換されます。
convert_to_array_ex(value) 配列への強制的な変換を行います。配列が渡された場合は何もしません。 オブジェクトは、すべてのプロパティが配列に変換されます。 プロパティ名が配列のキー、プロパティの内容が配列の値となります。 NULL 値は空の配列に変換されます。それ以外のすべての値は、 キー 0 に対応する値として元の値を格納した配列となります。
convert_to_object_ex(value) オブジェクトへの強制的な変換を行います。オブジェクトが渡された場合は何もしません。 NULL 値は空のオブジェクトに変換されます。配列は、そのキーをプロパティに、 その値をプロパティの内容として保持するオブジェクトに変換されます。 その他の型は、すべてプロパティ scalar をもつオブジェクトに変換されます。このプロパティの内容は、 変換前の値となります。
convert_to_null_ex(value) NULL 値、つまり空白になるように変換します。

注意: これらの振る舞いのデモが、付録 CD-ROM の cross_conversion.php で見られます。

PHP の型変換の挙動

引数に対してこれらの関数を使用することで、 渡されたデータの型の安全性を確保できます。 渡された型が要求と異なった場合、PHP は空の値 (空の文字列、配列、オブジェクトや 数値の 0、Boolean の FALSE など) を結果として返します。

これは、先ほど説明したサンプルモジュールのコードの一部を引用したものです。 ここで実際に変換関数を使用しています。

zval **parameter;

if((ZEND_NUM_ARGS() != 1) || (zend_get_parameters_ex(1, &parameter) != SUCCESS))
{
    WRONG_PARAM_COUNT;
}

convert_to_long_ex(parameter);

RETURN_LONG(Z_LVAL_P(parameter));
パラメータへのポインタを取得した後に、パラメータの値が long 型 (整数値) に変換されます。そしてそれがこの関数の返り値となります。 この値の中身にアクセスするには、zval 型についての知識が必要です。その定義を Zend API: PHP のコアをハックする に示します。

例7 PHP/Zend zval 型定義

typedef pval zval;
     
typedef struct _zval_struct zval;

typedef union _zvalue_value {
    long lval;                 /* long 値 */
    double dval;               /* double 値 */
    struct {
        char *val;
        int len;
    } str;
    HashTable *ht;             /* ハッシュテーブル値 */
    struct {
        zend_class_entry *ce;
        HashTable *properties;
    } obj;
} zvalue_value;

struct _zval_struct {
    /* 変数の情報 */
    zvalue_value value;        /* 値 */
    unsigned char type;        /* アクティブな型 */
    unsigned char is_ref;
    short refcount;
};

実際のところは pval (php.h で定義) は単なる zval (