言語を拡張する際の最も重要な問題のひとつは、 引数として渡されるデータの扱いです。たいていの拡張モジュールは、 何らかの入力データを扱う (あるいはパラメータを受け取って 特定の動作を行う) ように作られています。そして、 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; |
このマクロはデフォルトのエラーメッセージを表示し、呼び出し元に制御を戻します。 このマクロの定義もまた zend_API.h にあります。このような内容です。
ZEND_API void wrong_param_count(void); #define WRONG_PARAM_COUNT { wrong_param_count(); return; } |
パラメータのパース用の新しい 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_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, ¶m) == 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, ¶meter) != SUCCESS) WRONG_PARAM_COUNT; |
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, ¶m1, ¶m2, ¶m3, ¶m4) != 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; |
これを非常にうまく実装したのが、PHP の fsockopen() を処理するコードです。このコードは ext/standard/fsock.c にあり、 例46-6 で見ることができます。 このソースの中に知らない関数が含まれていたとしても、現時点では問題ありません。 すぐにわかるようになります。
fsockopen() が受け付ける引数の数は、 2、3、4、あるいは 5 です。お決まりの変数宣言の後に、 この関数は引数の数が範囲内にあるかどうかを調べます。 それから、switch() 文の下に流れていく性質を利用して、 すべての引数を処理します。switch() 文は、 まず引数の数が最大 (5 個) であった場合の処理から始まります。 その後、引数の数が 4 個であった場合の処理、3 個であった場合の処理、 と順にたどっていきます。これは、キーワード break の記述を省略しているからです。最後の処理を実行した後で switch() 文は終了し、 関数の引数が 2 個だけであった場合でも、必要最小限の処理が実行されます。
このように複数ステージの処理を段階的に実行するようにすると、 可変引数の処理がやりやすくなります。
引数にアクセスするには、すべての引数の型がきちんと定義されていなければなりません。 何度も言いますが、PHP は究極の動的言語なので、時に問題が起こることがあります。 PHP は一切の型チェックを行わないので、 どんな型のデータでも関数に渡すことができてしまいます。 本当は整数値を期待しているのに配列が渡されるかもしれないし、 その逆だってありえます。PHP は、こんな場合でも一切なにも通知しません。
これを防ぐには、これらの API を使用して引数の方を強制的に変換しなければなりません (表46-4 を参照ください)。
注意: すべての変換関数のパラメータは **zval です。
表 46-4. 引数の変換関数
関数 | 説明 |
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 で見られます。その出力を 図46-2 に示します。
引数に対してこれらの関数を使用することで、 渡されたデータの型の安全性を確保できます。 渡された型が要求と異なった場合、PHP は空の値 (空の文字列、配列、オブジェクトや 数値の 0、Boolean の FALSE など) を結果として返します。
これは、先ほど説明したサンプルモジュールのコードの一部を引用したものです。 ここで実際に変換関数を使用しています。
zval **parameter; if((ZEND_NUM_ARGS() != 1) || (zend_get_parameters_ex(1, ¶meter) != SUCCESS)) { WRONG_PARAM_COUNT; } convert_to_long_ex(parameter); RETURN_LONG(Z_LVAL_P(parameter)); |
例 46-7. PHP/Zend zval 型定義
|
実際のところは pval (php.h で定義) は単なる zval (zend.h で定義) のエイリアスであり、これは _zval_struct をさしています。 そこで、この構造体に注目してみましょう。 _zval_struct は「マスタ」構造体であり、 その中には値情報、型情報、参照情報が含まれています。 中に含まれている zvalue_value は共用体であり、 変数の値がそこに含まれます。変数の型に応じて、共用体の中の適切なメンバーにアクセスする必要があります。 それぞれの構造体については 表46-5、 表46-6 および 表46-7 を参照ください。
表 46-5. Zend zval 構造体
項目 | 説明 |
value | この変数の内容を含む共用体。詳細は 表46-6 を参照ください。 |
type | 変数の型を含みます。使用可能な型については 表46-7 を参照ください。 |
is_ref | この変数が参照ではない場合に 0、 他の変数への参照である場合に 1 となります。 |
refcount | この変数に対する参照の数。この変数に格納されている値への新しい参照が作成されるたびに、 カウンタが 1 増加します。参照が解除されるたびに、カウンタが 1 減少します。 参照カウンタが 0 になった段階で、この値はどこからも参照されていないことになります。 この時点で、自動的にメモリが解放されます。 |
表 46-6. Zend zvalue_value 構造体
項目 | 説明 |
lval | 変数の型が IS_LONG、 IS_BOOLEAN あるいは IS_RESOURCE である場合にこのプロパティを使用します。 |
dval | 変数の型が IS_DOUBLE である場合にこのプロパティを使用します。 |
str | 型が IS_STRING の変数にアクセスする際に、この構造体を使用します。 len には文字列の長さが含まれ、 val が文字列へのポインタとなります。Zend は C の文字列を使用しているので、 文字列の長さには、最後の 0x00 のぶんも含まれます。 |
ht | 変数が配列である場合に、この項目は配列のハッシュテーブルエントリへのポインタとなります。 |
obj | 変数の型が IS_OBJECT である場合にこのプロパティを使用します。 |
表 46-7. Zend 変数の型を表す定数
定数 | 説明 |
IS_NULL | NULL (空白) 値を表します。 |
IS_LONG | long (整数) 値。 |
IS_DOUBLE | double (浮動小数点) 値。 |
IS_STRING | 文字列。 |
IS_ARRAY | 配列を表します。 |
IS_OBJECT | オブジェクト。 |
IS_BOOL | Boolean 値。 |
IS_RESOURCE | リソース (リソースについては、 以下の適切なセクションを参照ください)。 |
IS_CONSTANT | 定数 (定義済み) 値。 |
long 値にアクセスするには zval.value.lval、 double 値にアクセスするには zval.value.dval、といったように使用します。 すべての値は共用体に保存されるので、不適切なメンバーにアクセスすると、 無意味な結果を得ることになります。
配列やオブジェクトへのアクセスは少し複雑なので、後で説明します。
参照渡しの引数を受け取って関数内部でそれを変更しようとする場合は、 少々注意が必要です。
敢えて説明しませんでしたが、ここで示している状況では、 関数のパラメータとして受け取った zval に対する書き込み権限はありません。 もちろん関数内で独自に作成した zval を変更することは可能ですが、 Zend の内部データを参照している zval は、 決して変更してはいけません。
これまで説明してきたのは、いわゆる *_ex() 系の API ばかりです。お気づきかもしれませんが、これまで使用してきた API 関数は zend_get_parameters() ではなくて zend_get_parameters_ex() でしたし、また convert_to_long() ではなくて convert_to_long_ex() でした。これらの *_ex() 系の関数は、いわゆる「拡張」Zend API と呼ばれるものです。 これらは、以前の API に比べて速度が少し向上しているのですが、 それと引き換えに読み込み専用のアクセスしかできないようになっています。
Zend は内部的には参照を使用しているので、 さまざまな変数が同じ値を参照することもありえます。 zval コンテナへの書き込みアクセスをするためには、 このコンテナが保持する値が他と完全に分離していること、 つまり他のコンテナから参照されていないことが必要です。 zval コンテナが他のコンテナから参照されている場合に その zval を変更すると、この zval を参照しているその他のコンテナの内容も変わってしまいます (それらも変更後の値を指すようになるからです)。
zend_get_parameters_ex() は、単に zval コンテナへのポインタを返します。そこに参照が含まれているかどうかは考慮しません。 一方、これに対応する伝統的な API である zend_get_parameters() は、参照をチェックします。 参照が見つかった場合には、独立した zval コンテナを新しく作成し、参照先のデータをそこにコピーし、 その新しく作成した (他とは分離している) コンテナへのポインタを返します。
この操作のことを zval separation (zval の分離) (あるいは pval separation) と言います。*_ex() API は zval の分離を行わないません。そのために大幅に高速化されましたが、 その代償として書き込みアクセスができなくなっています。
とは言うものの、パラメータを変更するには書き込みアクセスをしなければなりません。 このような場合のために、Zend には特別な方法が用意されています。 関数のパラメータが参照渡しされた場合は、自動的に zval の分離が行われるのです。 つまり、PHP で下のように関数をコールすると、$parameter が独立した値として渡されることを Zend が自動的に保証してくれるのです。 これにより、書き込みが可能となります。
my_function(&$parameter); |
しかし、これは値渡しのパラメータには適用されません! 参照渡し以外で渡されたパラメータ以外は、読み込み専用となります。
これらの性質により、パラメータを扱う際にはそれが参照なのかそうでないのかをしっかり見極める必要があります。 さもないと、思ってもいない結果を引き起こすことになります。 パラメータが参照渡しされたかどうかを調べるためには、マクロ PZVAL_IS_REF を使用します。このマクロは zval* を受け取り、それが参照かそうでないかを返します。 例46-8 に実際の使用例があります。
例 46-8. パラメータが参照で渡されたかどうかを調べる
|
zend_get_parameters_ex() で取得したパラメータのうち、 参照渡しではないものについても値を変更したくなる場合があるかもしれません。 そんなときには、マクロ SEPARATE_ZVAL を使用します。 これは、指定したコンテナについて zval の分離を行います。 新しく作成された zval は内部のデータとは分離されており、 ローカルスコープでしか使用できません。つまり、 スクリプト全体のコンテキストに影響を与えることなく データを変更したり破壊したりできるようになるのです。
zval **parameter; /* パラメータを取得します */ zend_get_parameters_ex(1, ¶meter); /* この時点では、<parameter> はまだ */ /* Zend の内部データバッファと紐付いています */ /* <parameter> を書き込み可能にします */ SEPARATE_ZVAL(parameter); /* この時点で、<parameter> が変更できるようになります。*/ /* グローバルに影響を与えることはありません */ |
注意: 書き込み権限の問題については、 「伝統的な」API (zend_get_parameters() やその他) を使用すれば簡単に回避できます。しかし、この API は非推奨のようなので、今後この章では深入りしません。