はじめに
Swiftとかだとextension
を使って、元から存在する型(String, arrayとか)に任意の関数を追加することができます。
extension Int {
static var zero: Int { return 0 }
mutating func inverse() {
self = -self
}
}
しかし、PHPのエクステンションはC言語で書かれています。そのため実装は大変なのですが、普通のPHPスクリプトと異なり高速で動作します。このエクステンションはPHP7 と PHP5 で大幅に変わったらしいのですが、基本的には変わっていないように感じました。
準備
PHP本体のソースを取得
$ 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
...
ここでは、PHP7.1.2 をチェックアウトしていきます。
$ git checkout php-7.1.2
雛形の作成
$ 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
で表されます。
なので、dnl
を消してください。
$ 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 〜
という表示が出れば準備完了です。
自作関数の登録と実装
今回は簡単なadd関数を自作することにします。add_extension
という関数名にしておきます。使用例としては
$a = 5;
$b = 5;
pr(add_extension($a, $b)); // 10
関数の登録
今回はconst zend_function_entry add_extension_functions[]
にadd_extension
関数を追加して行きます。変数名は特に指定されていないのでわかりやすくしておきましょう。
// Reflectionなし
const zend_function_entry add_extension_functions[] = {
PHP_FE(add_extension, NULL)
PHP_FE_END
};
// Reflectionあり
ZEND_BEGIN_ARG_INFO_EX(arginfo_add_extension_functions, 0, 0, 2)
ZEND_ARG_INFO(0, val1)
ZEND_ARG_INFO(0, val2)
ZEND_END_ARG_INFO()
const zend_function_entry add_extension_functions[] = {
PHP_FE(add_extension, arginfo_add_extension_functions)
PHP_FE_END
};
PHP_FE(add_extension, NULL)
の第2引数にarg_info
という構造体を渡すとタイプヒンティングやリフレクション等が利用できるようになります。この説明については付録で書きます。
関数本体の実装
関数本体を実装するときにはPHP_FUNCTION()
というマクロを利用します。
PHP_FUNCTION(add_extension) {
// 引数の格納先
zend_long val1, val2;
// 引数をパースして格納先に代入
ZEND_PARSE_PARAMETERS_START(2, 2)
Z_PARAM_LONG(val1)
Z_PARAM_LONG(val1)
ZEND_PARSE_PARAMETERS_END();
// 足し算
zend_long res = val1 + val2;
// 結果を return する
RETURN_LONG(res);
}
ZEND_PARSE_PARAMETERS_START(MIN, MAX)
第1引数は最小引数、第2引数は最大引数の個数を表します。
Z_PARAM
受け取る引数の型を指定して、どの変数に代入するかを指定します。型指定子
とよばれ以下のように定義されています。
specifier | Fast ZPP API macro | args |
---|---|---|
| | Z_PARAM_OPTIONAL | |
a | Z_PARAM_ARRAY(dest) | dest - zval* |
A | Z_PARAM_ARRAY_OR_OBJECT(dest) | dest - zval* |
b | Z_PARAM_BOOL(dest) | dest - zend_bool |
C | Z_PARAM_CLASS(dest) | dest - zend_class_entry* |
d | Z_PARAM_DOUBLE(dest) | dest - double |
f | Z_PARAM_FUNC(fci, fcc) | fci - zend_fcall_info, fcc - zend_fcall_info_cache |
h | Z_PARAM_ARRAY_HT(dest) | dest - HashTable* |
H | Z_PARAM_ARRAY_OR_OBJECT_HT(dest) | dest - HashTable* |
l | Z_PARAM_LONG(dest) | dest - long |
L | Z_PARAM_STRICT_LONG(dest) | dest - long |
o | Z_PARAM_OBJECT(dest) | dest - zval* |
O | Z_PARAM_OBJECT_OF_CLASS(dest, ce) | dest - zval* |
p | Z_PARAM_PATH(dest, dest_len) | dest - char*, dest_len - int |
P | Z_PARAM_PATH_STR(dest) | dest - zend_string* |
r | Z_PARAM_RESOURCE(dest) | dest - zval* |
s | Z_PARAM_STRING(dest, dest_len) | dest - char*, dest_len - int |
S | Z_PARAM_STR(dest) | dest - zend_string* |
z | Z_PARAM_ZVAL(dest) | dest - zval* |
Z_PARAM_ZVAL_DEREF(dest) | dest - zval* | |
+ | Z_PARAM_VARIADIC('+', dest, num) | dest - zval*, num int |
* | Z_PARAM_VARIADIC('*', dest, num) | dest - zval*, num int |
実際に呼び出してみる
<?php
$val1 = 10;
$val2 = 10;
$res = add_extension($val1, $val2);
echo "$res\n";
$ php -d extension=./modules/myext.so myext_test.php
20
とりあえず関数のエクステンションを作り上げることまではできるようになりました。
記事を書く気力とプログラムを組む気力があれば、エクステンションでクラス作成もしてみたいと思います。
付録
PHP_FE
arg_info構造体
PHP_FE
マクロの第2引数はReflection APIに対して関数の引数情報を提供するために使用されます。つまりは指定しなくても(NULL
でも)動きますが、実装したExtensionを後悔する場合には必ずと言っていいほど必要になるので、実装しておく方が無難です。以下にmb_convert_encoding
関数を例に示します。
mb_convert_encoding
// 文字列 strの文字エンコーディングを
// オプションで指定した from_encoding から to_encoding に変換します。
string mb_convert_encoding (
string $str, string $to_encoding [, mixed $from_encoding = mb_internal_encoding() ]
)
この定義がされているphp/ext/mbstring/mbstring.c
には以下のように記載されています
ZEND_BEGIN_ARG_INFO_EX(arginfo_mb_convert_encoding, 0, 0, 2)
ZEND_ARG_INFO(0, str)
ZEND_ARG_INFO(0, to)
ZEND_ARG_INFO(0, from)
ZEND_END_ARG_INFO()
PHP_FE(mb_convert_encoding, arginfo_mb_convert_encoding)
ZEND_BEGIN_ARG_INFO_EX
以下の4つの引数を指定します。
引数番号 | 解説 |
---|---|
1 | 定義名(PHP_FEマクロの第2引数として指定する文字列) |
2 | 未使用 |
3 | 返却フラグ(1: 参照を返す, 0: 受け取るだけ) |
4 | 必須引数の個数 |
ZEND_ARG_INFO
関数の引数の数だけ記述する必要があります。それぞれ以下の2つの引数を指定します。
引数番号 | 解説 |
---|---|
1 | 1: 参照渡し, 0: 値渡し |
2 | 仮引数名 |
ZEND_END_ARG_INFO
引数定義の終了を示します。
先ほどのを見返すと
ZEND_BEGIN_ARG_INFO_EX(arginfo_mb_convert_encoding, 0, 0, 2)
ZEND_ARG_INFO(0, str)
ZEND_ARG_INFO(0, to)
ZEND_ARG_INFO(0, from)
ZEND_END_ARG_INFO()
PHP_FE(mb_convert_encoding, arginfo_mb_convert_encoding)
となっています。必須引数が2となっていますが、これはZEND_ARG_INFO
の定義順で必須になっていくので注意してください。また、ZEND_ARG_INFO
の第1引数が0なので全て値渡しになります。
このPHP_FE
やZEND_ARG_INFO
で行なった引数定義はReflectionFunction
クラスを用いて参照することができます
// $ cat reflection_exec.php
$reffunc = new ReflectionFunction('mb_convert_encoding');
foreach ($reffunc->getParameters() as $arg) {
print $arg . PHP_EOL;
}
// $ php reflection_exec.php
Parameter #0 [ <required> $str ]
Parameter #1 [ <required> $to ]
Parameter #2 [ <optional> $from ]