はじめに
PHP エクステンションは C 言語で書かれる、 PHP 機能を拡張するモジュールです。普通の PHP スクリプトと比べてかなり速度が出るため、重い計算が必要な箇所などでうまく使っていければ強い味方になると思います。
PHP7 は PHP5 とくらべてエクステンションの書き方が大幅に変わったそうです。この新しい書き方の説明やチュートリアルが少なく、自作しようとしたときにかなり苦労をしました。
なんとか書けるようになってきたのでその過程で調べたことをメモしてみます。なお、自分はエクステンション自体( PHP5 でも)ほとんど書いたことはありませんでした。
※PHP7で大幅に変わったのは後半のカスタムオブジェクトのお話のあたりで、前半部分は PHP5 とさほど変わらないようです。
なお、今回作成したサンプルのコードは以下で公開しています:
https://github.com/maple-nishiyama/extension-in-php7
準備
バージョン確認
まず、自分の PC の PHP のバージョンを確認します。
$ php -v
PHP 7.1.2 (cli) (built: Feb 17 2017 10:51:21) ( NTS )
Copyright (c) 1997-2017 The PHP Group
Zend Engine v3.1.0, Copyright (c) 1998-2017 Zend Technologies
作ったら自分で使いたいことがほとんどだと思いますので、今回はこのバージョンに合わせたエクステンションを作ることにします。
PHP 本体のソースコードの入手
エクステンションのビルドには本体のソースコードが必要です。 GitHub から clone してきます:
$ git clone git@github.com:php/php-src.git
$ cd php-src
先程確認したバージョンをチェックアウトする
$ git tag --list
...
php-7.1.0
php-7.1.0RC1
php-7.1.0RC2
php-7.1.0RC3
php-7.1.0RC4
php-7.1.0RC5
php-7.1.0RC6
php-7.1.0alpha1
php-7.1.0alpha2
php-7.1.0alpha3
php-7.1.0beta1
php-7.1.0beta2
php-7.1.0beta3
php-7.1.1
php-7.1.1RC1
php-7.1.2
php-7.1.2RC1
php-7.1.3
php-7.1.3RC1
...
で確認して、7.1.2 をチェックアウトしたいので、
$ git checkout php-7.1.2
で OK です。
エクステンションの雛形(スケルトン)を作成
エクステンションの作成にはソースコードの他にビルド設定ファイルなどが必要になりますが、
それらの雛形を生成してくれるコマンドが PHP 本体のソースコードに付属しているのでそれを使います。
$ cd ext
$ ./ext_skel --extname=myext
...
To use your new extension, you will have to execute the following steps:
1. $ cd ..
2. $ vi ext/myext/config.m4
3. $ ./buildconf
4. $ ./configure --[with|enable]-myext
5. $ make
6. $ ./sapi/cli/php -f ext/myext/myext.php
7. $ vi ext/myext/myext.c
8. $ make
...
ビルド/実行確認
雛形に最低限の編集をしてビルドと実行ができるか確認してみます。
$ cd myext
config.m4
を編集します。10行目あたりの
PHP_ARG_WITH(myext, for myext support,
Make sure that the comment is aligned:
[ --with-myext Include myext support])
という箇所のコメントを外します。(行頭の dnl という3文字を削除する)
$ phpize
$ ./configure
$ make
$ php -d extension=./modules/myext.so myext.php
Functions available in the test extension:
confirm_myext_compiled
Congratulations! You have successfully modified ext/myext/config.m4. Module myext is now compiled into PHP.
-d extension=./modueles/myext.so
で .so ファイルをロードするように指示する。
上記のような Congratulations! You have successfully 〜
という表示が出れば成功。
ここまでで準備は完了です。
自作の関数を追加する
練習として、2つの整数 a
, b
を受け取り、それらの和 a + b
を返す関数 my_sum($a, $b)
をエクステンションの関数として定義してみたいと思います。
関数の登録と実装
-
const zend_function_entry myext_functions[]
に my_sum 関数を登録
const zend_function_entry myext_functions[] = {
PHP_FE(confirm_myext_compiled, NULL) /* For testing, remove later. */
PHP_FE(my_sum, NULL) /* <------- これを追加 */
PHP_FE_END /* Must be the last line in myext_functions[] */
};
PHP_FE マクロの第2引数に arg_info という構造体を与えるとタイプヒンティングやリフレクションなどが有効になるようですが、ここでは割愛します。
- my_sum の本体を実装
PHP 側から呼び出すエクステンション関数は PHP_FUNCTION()
というマクロを使用して定義します:
PHP_FUNCTION(my_sum)
{
/* 引数の格納先 */
zend_long a, b;
/* 引数をパースして a, b に代入 */
ZEND_PARSE_PARAMETERS_START(2, 2)
Z_PARAM_LONG(a)
Z_PARAM_LONG(b)
ZEND_PARSE_PARAMETERS_END();
/* 和を計算 */
zend_long c = a + b;
/* 結果を return する */
RETURN_LONG(c);
}
PHP 側から呼び出してみる
ビルドできたら、 PHP 側で以下のようなスクリプトを書いてエクステンション関数を呼び出してみます:
$a = 10;
$b = 5;
$c = my_sum($a, $b);
echo "$c\n";
$ php -d extension=./modules/myext.so myext_test.php
15
うまくいきました。
ここまででもかなりのことができるようになると思います。
クラスの作成
エクステンションのコードで PHP のクラスを記述してみます。
クラスエントリー構造体の宣言と登録
zend_class_entry*
を global 変数として宣言します:
zend_class_entry* myext_ce;
クラス構造体を Zend エンジンに登録します:
PHP_MINIT_FUNCTION(myext)
{
zend_class_entry ce;
INIT_CLASS_ENTRY(ce, "Myext", NULL);
myext_ce = zend_register_internal_class(&ce);
return SUCCESS;
}
これで、まだメソッドもプロパティもないものの、 Myext クラスが PHP 側から new できるようになります:
$myext = new Myext();
var_dump($myext);
プロパティの追加
マクロ PHP_MINIT_FUNCTION(myext)
にて、プロパティの宣言と初期値の設定を行うことができます:
PHP_MINIT_FUNCTION(myext) {
...
zend_declare_property_string(myext_ce, "hello", sizeof("hello") - 1, "hello, myext!", ZEND_ACC_PUBLIC);
return SUCCESS;
}
PHP 側から new
して var_dump()
してみると、
object(Myext)#1 (1) {
["hello"]=>
string(13) "hello, myext!"
}
となり、プロパティが追加されていることが確認できます。
メソッドの追加
上で作った my_sum
関数と同じ内容で my_sum_method
というメソッドを追加してみます:
関数の追加には PHP_FUNCTION()
というマクロを使いましたが、メソッドの追加には
PHP_METHOD()
というマクロを使います:
PHP_METHOD(Myext, my_sum_method)
{
zend_long a, b;
ZEND_PARSE_PARAMETERS_START(2, 2)
Z_PARAM_LONG(a)
Z_PARAM_LONG(b)
ZEND_PARSE_PARAMETERS_END();
zend_long c = a + b;
RETURN_LONG(c);
}
作成したメソッドはメソッドリストに格納し、クラス登録時に設定します:
const zend_function_entry myext_methods[] = {
PHP_ME(Myext, my_sum_method, NULL, ZEND_ACC_PUBLIC)
PHP_FE_END /* Must be the last line in myext_functions[] */
};
INIT_CLASS_ENTRY(ce, "Myext", myext_methods);
動作確認
<?php
$ext = new Myext();
$c = $ext->my_sum_method(3, 4);
echo "$c\n"; // => 7
カスタムオブジェクトを利用するクラス
ここが今回主に書きたかったところです。
Cのコード内に自作の構造体を保持して、変数やバッファを内部で持ち回ることを考えます。
とくに大きな配列などを zval
のリストではなく、 double*
など C のポインタで扱えるので
パフォーマンスの向上が期待できます。
カスタムオブジェクト構造体
PHP の標準オブジェクト(クラスのインスタンスもオブジェクト型ですね)を拡張する形で
カスタムオブジェクト構造体を定義します。
これは myext.c ではなく、ヘッダファイルの php_myext.h に書くのが普通のようです。
/*
* カスタムオブジェクト
*/
typedef struct _php_myext {
zend_long size; // long 型のカスタムフィールド
double* buff; // double* 型のカスタムフィールド
zend_object std; // 標準オブジェクトを内包
} php_myext;
今回は double 型のバッファを指すのに使うポインタと、バッファの大きさを保持する long 型の変数をカスタムフィールドとして追加してみました。これらの他に、もともとの zend_object 構造体 std をメンバーに持っています。
std が構造体の末尾に位置していることに注意します。PHP5では先頭に位置していたのですが、プロパティの数を可変にするため(?)かなにかの理由で末尾に移動しています。
Zend エンジン側は独自定義した型 php_myext を知らないため、 zend_object 型のポインタとして扱ってもらうしかありませんが、この zend_object* から、php_myext* ポインタを得るには、上記のように std が末尾にあるせいで一工夫必要になります。
すなわち、下図のように php_myext 構造体の頭は、その中に持っている zend_object 構造体の頭より前にあるので、単に zend_object型ポインタそのままを php_myext* 型にキャストし直すだけではダメで、図のオフセット分前にずらしたアドレスを php_myext* 型にキャストしなければなりません。
これは次のような関数で行うのが標準のようです:
static inline php_myext* php_myext_fetch_object(zend_object* obj)
{
return (php_myext*)((char*)obj - XtOffsetOf(php_myext, std));
}
マクロ XtOffsetOf(php_myext, std)
が図のオフセットを計算してくれます。
また、 zval のポインタから php_myext のポインタを一気に取得できる次のマクロを定義しておくと便利です:
#define Z_MYEXT_OBJ_P(zv) php_myext_fetch_object(Z_OBJ_P(zv))
object handler
object handler はオブジェクトのコピーや破棄を行う関数を登録するための構造体のようです。
php_myext オブジェクトを扱うための object handler を宣言しておきます:
static zend_object_handlers myext_object_handlers;
カスタムオブジェクトの生成関数(コンストラクタ)を定義
ここでの生成関数は C 側のカスタムオブジェクトを生成するための関数です。 PHP 側からクラスを new するときに呼ばれる、いわゆるコンストラクタ ( `function __sturct() { } ) とは異なります。
static zend_object* php_myext_new(zend_class_entry* ce)
{
// メモリ領域を確保
php_myext* obj = (php_myext*)ecalloc(1, sizeof(php_myext) + zend_object_properties_size(ce));
// 標準オブジェクト部分の初期化
zend_object_std_init(&obj->std, ce);
// プロパティ部分の初期化
object_properties_init(&obj->std, ce);
obj->std.handlers = &myext_object_handlers;
return &obj->std;
}
ecalloc
は calloc
と同じ機能ですが、 Zend エンジンのメモリー管理機構を活用してくれるものらしいです。
カスタムオブジェクトの破棄関数(デストラクタ)を定義
// 破棄関数(デストラクタ)
static void php_myext_destroy_object(zend_object* object)
{
php_myext* obj = php_myext_fetch_object(object);
// ここでは obj->buff を解放してはならない
zend_objects_destroy_object(object); // PHP 側で __destruct() が呼ばれる。
}
static void php_myext_object_free_storage(zend_object* object)
{
php_myext* obj = php_myext_fetch_object(object);
if (obj->buff) {
efree(obj->buff);
}
zend_object_std_dtor(&obj->std);
}
破棄関数には2種類あって、*_object_free_storage
のほうでは独自確保したバッファの解放などを行います。
*_destroy_object
のほうでは例えばバッファの内容をソケットに送信したりファイルに書き込んでしまうような処理を行うと想定らしいです。
*_destroy_object
のほうで zend_objects_destroy_object
を呼ぶことで、 PHP 側のデストラクタ __destruct()
がコールされる仕組みになっています。
生成・破棄関数の登録
作成した生成関数をクラスエントリー構造体に登録します。
また、破棄関数を object handler に登録します。
PHP_MINIT_FUNCTION(myext)
{
...
// 生成関数を登録
myext_ce->create_object = php_myext_new;
// object handler を初期化して
memcpy(&myext_object_handlers, zend_get_std_object_handlers(), sizeof(zend_object_handlers));
// 破棄関数などを登録
myext_object_handlers.offset = XtOffsetOf(php_myext, std);
myext_object_handlers.clone_obj = NULL; // 今回は未調査
myext_object_handlers.dtor_obj = php_myext_destroy_object;
myext_object_handlers.free_obj = php_myext_object_free_storage;
return SUCCESS;
}
ここまでで、カスタムオブジェクトを用いたクラスの構成はとりあえず完了です。
以下では追加したカスタムフィールドへのアクセスの仕方や、PHP の new を用いずに C 言語側で直接カスタムオブジェクトを生成して PHP 側に返すような処理の書き方を見てみます。
PHP 側のコンストラクタとバッファの読み書き処理
PHP 側のコンストラクタ関数 ( function __construct() ) を定義します。
引数として PHP の数値配列を受け取って、それらの数値をカスタムフィールド buff が指すバッファにコピーする処理を行ってみます。
// PHP 側のコンストラクタ (__construct() メソッド)
// PHP の配列を受け取って、それと同じ値が並ぶCの配列を確保する
PHP_METHOD(Myext, __construct)
{
zval* a;
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_ARRAY(a)
ZEND_PARSE_PARAMETERS_END();
HashTable* h = HASH_OF(a);
php_myext* object = Z_MYEXT_OBJ_P(getThis());
// C側のメモリーをここで確保
zend_long s = zend_hash_num_elements(h);
object->size =s;
object->buff = ecalloc(s, sizeof(double));
// Cの配列に値をコピーしていく
for (zend_long i = 0; i < s; ++i) {
zval* elm = zend_hash_index_find(h, i);
convert_to_double_ex(elm);
object->buff[i] = Z_DVAL_P(elm);
}
}
次に、C 側のバッファの内容を PHP の配列として読み出すメソッドも考えてみます:
PHP_METHOD(Myext, readBuffer)
{
php_myext* object = Z_MYEXT_OBJ_P(getThis());
zend_long s = object->size;
zval out;
array_init_size(&out, s);
for (zend_long i = 0; i < s; ++i) {
add_index_double(&out, i, object->buff[i]);
}
RETURN_ZVAL(&out, 1, 1);
}
最後に、C 側で直接カスタムオブジェクトを生成して PHP 側に返すような関数を書いてみます:
PHP_METHOD(Myext, zeros)
{
zend_long s;
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_LONG(s)
ZEND_PARSE_PARAMETERS_END();
// zval* return_value という返り値用の変数がマクロ内で定義されているので
// それをほしい形に加工する
object_init_ex(return_value, myext_ce);
Z_SET_REFCOUNT_P(return_value, 1);
php_myext* object = Z_MYEXT_OBJ_P(return_value);
object->size = s;
object->buff = ecalloc(s, sizeof(double));
}
これらはもちろんメソッドリストに入れておきます。
const zend_function_entry myext_methods[] = {
PHP_ME(Myext, __construct, NULL, ZEND_ACC_PUBLIC)
PHP_ME(Myext, readBuffer, NULL, ZEND_ACC_PUBLIC)
PHP_ME(Myext, my_sum_method, NULL, ZEND_ACC_PUBLIC)
PHP_ME(Myext, zeros, NULL, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
PHP_FE_END /* Must be the last line in myext_functions[] */
};
これでカスタムオブジェクトを用いて独自のデータをC側で持ち回る処理をひと通り見ました。
終わりに
PHP7でのエクステンションの書き方を調べた結果をメモとして残しました。
- 素のエクステンション関数の書き方
- クラス/プロパティ/メソッドの書き方
- カスタムオブジェクトを用いたクラスの書き方
を学びました。
今回触れられなかった題材として、リソース(ストリーム)の扱いがありますが、またどこかで。