Posted at

PHP Extensionの開発①

More than 1 year has passed since last update.


概要

以前、PHP Extensionの開発環境について紹介しました。

今回は、簡単な開発についてまとめています。


前提

以下は、PHP Extensionの開発前に、最低限知っておいたほうがよいことです。

メモリ管理なども知っていると理解が進みます(ガベージコレクション


導入

前提を満たした上で、まずはPHPのソースを読み、利用できるデータ型やマクロについて理解を深めたほうがよいでしょう。

例えば、よく利用されるデータ型やマクロに関しては、zend.hから追っていけば理解できると思います。

https://github.com/php/php-src/blob/master/Zend/zend.h

また、以下で定義されているAPIもよく利用されています。

https://github.com/php/php-src/blob/master/Zend/zend_API.h


導入には比較的やさしいPHPの標準関数であるarray_searchを例に、C言語で書かれた処理を理解します。

なお配列に関する標準関数は、array.cから確認できます。


array_search

まずは、各マクロやAPI関数がどのように利用されているか理解します。

PHP_FUNCTION

関数作成時に利用するマクロです。

リンク先で詳しく述べているのでそちらを参考にしたほうがよいでしょう。

特筆すべきは、return_valuePHP 変数へのポインタ。ここに、ユーザーへの戻り値を設定します。という点です。

引数として指定しているINTERNAL_FUNCTION_PARAM_PASSTHRUは、execute_datareturn_valueへ展開され、関数内部で利用されます。

引数は、以下のマクロを介して関数内で利用されます。

ZEND_PARSE_PARAMETERS_START(2, 3)

Z_PARAM_ZVAL(value)
Z_PARAM_ARRAY(array)
Z_PARAM_OPTIONAL
Z_PARAM_BOOL(strict)
ZEND_PARSE_PARAMETERS_END();

ZEND_PARSE_PARAMETERS_START

こちらに記載されているように、引数は第1が最小引数、第2が最大引数を表します。

Z_PARAM_ZVAL(value)

array_searchの第1引数

Z_PARAM_ARRAY(array)

array_searchの第2引数

Z_PARAM_OPTIONAL

Z_PARAM_BOOL(strict)

array_searchの第3引数(オプショナル)

array_searchの公式リファレンスに記載の以下引数の数や値の型が合っていることを確認できます。

mixed array_search ( mixed $needle , array $haystack [, bool $strict = false ] )

ZEND_HASH_FOREACH_KEY_VAL

いわゆるforeachと同義で、以下の例の場合、

$array = array('blue' => '青', 'red' => '赤', 'green' => '緑', 'black' => '黒');

$key = array_search('緑', $array); // $key = 2;

ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(array), num_idx, str_idx, entry)の各値は以下のようになります。

num_idx(数値の添字)
str_idx(文字列の添字)
entry(要素)

$array[0]
0
blue

$array[1]
1
red

$array[2]
2
green

$array[3]
3
black

fast_is_identical_function

引数に指定された値を比較し、異なれば0(false)、一致すればそれ以外(true)を結果として返します。

RETVAL_STR_COPY

返り値として、文字列を返したい場合に利用します。

array_searchの場合、引数で指定された配列が連想配列だった場合などに、そのキーを返します。

RETVAL_LONG

数値(Long型)を返したい場合に利用します。

返り値に関しては、主に2種類のマクロ(RETURN_*マクロとRETVAL_*マクロ)を利用します。

RETURN_ マクロはその場で関数の実行を終了します (つまり、return; します) が、 RETVAL_ は、return_value を設定してからも実行を続けます。

以上のように各マクロやAPI関数を順に追っていけば、そのふるまいを理解できると思います。


開発前準備

さすがにvimだとソースのトレースなどに時間がかかるためIDE(統合開発環境)を利用します。

私はVisual Studio Codeを利用しています。

導入手順はこちらが分かりやすいです。

そして、php-srcはmasterブランチをチェックアウトし利用します。

また、それに伴いdocker環境のphpバージョンを上げます。

$ vim web/Dockerfile

FROM php:7.1.3-apache

$ docker-compose build
Building web
.....
Successfully built b9ea3a759cc7

$ docker-compose up -d
$ docker-compose run web php -v
PHP 7.1.3 (cli) (built: Mar 21 2017 23:15:20) ( NTS )

PHPのバージョンが変わったので、PHP Extensionスケルトンプロジェクトも以前と同じように作り直しておきます。

(extension_dirが変わっているので注意が必要です)

php -i | grep extension_dir

extension_dir => /usr/local/lib/php/extensions/no-debug-non-zts-20160303 => /usr/local/lib/php/extensions/no-debug-non-zts-20160303


開発

理解の意味でもarray_searchを利用した関数を作成してみます。

はじめに注意点として、ヘッダ(php_array.h)に宣言されている関数でも外部からは直接呼ぶことはできないということです。

試しに以下のように、array_search関数を呼び出してみると、ビルドは通りますが実際に利用するとエラーが出力されます。

#include "ext/standard/php_array.h"

.....
zif_array_search(INTERNAL_FUNCTION_PARAM_PASSTHRU);

php: symbol lookup error: /usr/local/lib/php/extensions/no-debug-non-zts-20160303/helloworld.so: undefined symbol: zif_array_search

そもそも各関数はスタンドアロンで動作するよう設計されており、外部からの呼び出しは禁止されているようです。

ただし例外もあり、PHPAPIとして宣言されている関数などは利用可能です。

今回はarray.cに定義されているphp_search_array関数をhelloworld.cに直接コピーし利用することにします。

array_searchの情報を出力する関数を以下のように作ってみます。


helloworld.c

const zend_function_entry helloworld_functions[] = {

PHP_FE(confirm_helloworld_compiled, NULL) /* For testing, remove later. */
PHP_FE(array_search_with_info, NULL)
PHP_FE_END /* Must be the last line in helloworld_functions[] */
};

PHP_FUNCTION(array_search_with_info)
{
zval *key,
*array,
*entry;
zend_ulong num_idx;
zend_string *str_idx;
ZEND_PARSE_PARAMETERS_START(2, 2)
Z_PARAM_ZVAL(key)
Z_PARAM_ARRAY(array)
ZEND_PARSE_PARAMETERS_END();

zend_printf("--- function info ---\n");
zend_execute_data *ex = EG(current_execute_data);
if (ex && (ex->func)) {
zend_printf("function name=%s \n", ex->func->common.function_name->val);
}

if (Z_TYPE_P(key) == IS_LONG) {
zend_long arg = Z_LVAL_P(key);
zend_printf("numerical arg=%d \n", arg);
} else if (Z_TYPE_P(key) == IS_STRING) {
zend_string *arg = Z_STR_P(key);
zend_printf("string arg=%s \n", arg->val);
}

ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(array), num_idx, str_idx, entry) {
if (str_idx) {
zend_printf("string key=%s \n", str_idx->val);
} else {
zend_printf("numerical key=%d \n", num_idx);
}
if (Z_TYPE_P(entry) == IS_LONG) {
zend_long value = Z_LVAL_P(entry);
zend_printf("numerical value=%d \n", value);
} else if (Z_TYPE_P(entry) == IS_STRING) {
zend_string *value = Z_STR_P(entry);
zend_printf("string value=%s \n", value->val);
}
} ZEND_HASH_FOREACH_END();

php_search_array(INTERNAL_FUNCTION_PARAM_PASSTHRU, 1);

if (Z_TYPE_P(return_value) == IS_LONG) {
zend_long rval = Z_LVAL_P(return_value);
zend_printf("return numerical value=%d \n", rval);
} else if (Z_TYPE_P(return_value) == IS_STRING) {
zend_string *rval = Z_STR_P(return_value);
zend_printf("return string value=%s \n", rval->val);
}
zend_printf("--- end function info ---\n");
return;
}



test.php

<?php

dl('helloworld.so');

$array = array(0 => 'blue', 1 => 'red', 2 => 'green', 3 => 'black');
$key = array_search_with_info('green', $array);
echo "result=" . "$key\n";

$array = array('blue' => 0, 'red' => 1, 'green' => 2, 'black' => 3);
$key = array_search_with_info(2, $array);
echo "result=" . "$key\n";
?>


以下は、test.phpの実行結果です。

--- function info ---

function name=array_search_with_info
string arg=green
numerical key=0
string value=blue
numerical key=1
string value=red
numerical key=2
string value=green
numerical key=3
string value=black
return numerical value=2
--- end function info ---
result=2
--- function info ---
function name=array_search_with_info
numerical arg=2
string key=blue
numerical value=0
string key=red
numerical value=1
string key=green
numerical value=2
string key=black
numerical value=3
return string value=green
--- end function info ---
result=green

array_searchと同じ結果を返すとともに、実行した関数名・指定した引数・返り値を表示しています。

基本的に、array_searchで利用されているマクロを追い、以下関連する共用・構造体を見れば理解できると思います。

EG(current_execute_data)マクロに関しては、Executor Globals - EG() Changesを参考ください。


zend_compile.h

struct _zend_execute_data {

const zend_op *opline; /* executed opline */
zend_execute_data *call; /* current call */
zval *return_value;
zend_function *func; /* executed function */
zval This; /* this + call_info + num_args */
zend_execute_data *prev_execute_data;
zend_array *symbol_table;
#if ZEND_EX_USE_RUN_TIME_CACHE
void **run_time_cache; /* cache op_array->run_time_cache */
#endif
#if ZEND_EX_USE_LITERALS
zval *literals; /* cache op_array->literals */
#endif
};


zend_types.h

struct _zval_struct {

zend_value value; /* value */
union {
struct {
ZEND_ENDIAN_LOHI_4(
zend_uchar type, /* active type */
zend_uchar type_flags,
zend_uchar const_flags,
zend_uchar reserved) /* call info for EX(This) */
} v;
uint32_t type_info;
} u1;
union {
uint32_t next; /* hash collision chain */
uint32_t cache_slot; /* literal cache slot */
uint32_t lineno; /* line number (for ast nodes) */
uint32_t num_args; /* arguments number for EX(This) */
uint32_t fe_pos; /* foreach position */
uint32_t fe_iter_idx; /* foreach iterator index */
uint32_t access_flags; /* class constant access flags */
uint32_t property_guard; /* single property guard */
uint32_t extra; /* not further specified */
} u2;
};


zend_types.h

typedef union _zend_value {

zend_long lval; /* long value */
double dval; /* double value */
zend_refcounted *counted;
zend_string *str;
zend_array *arr;
zend_object *obj;
zend_resource *res;
zend_reference *ref;
zend_ast_ref *ast;
zval *zv;
void *ptr;
zend_class_entry *ce;
zend_function *func;
struct {
uint32_t w1;
uint32_t w2;
} ww;
} zend_value;


zend_compile.h

union _zend_function {

zend_uchar type; /* MUST be the first element of this struct! */
uint32_t quick_arg_flags;

struct {
zend_uchar type; /* never used */
zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */
uint32_t fn_flags;
zend_string *function_name;
zend_class_entry *scope;
union _zend_function *prototype;
uint32_t num_args;
uint32_t required_num_args;
zend_arg_info *arg_info;
} common;

zend_op_array op_array;
zend_internal_function internal_function;
};


関連する構造体を見ると、そこで定義されている変数の多さに抵抗があるかもしれませんが、開発する上では完全に覚える必要はありません。(もちろん理解したほうが面白いと思います)