PHPエクステンションで自作関数を作る!(PHP7向け)

f:id:nomunomu0504:20190411144524j:plain:w0

はじめに

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関数を追加して行きます。変数名は特に指定されていないのでわかりやすくしておきましょう。

// add_extension.c
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: rfc:fast_zpp

実際に呼び出してみる

 <?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_FEZEND_ARG_INFOで行なった引数定義はReflectionFunctionクラスを用いて参照することができます

// $ cat reflection_exec.php
<?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 ]