モダンなPHP拡張の書き方

Copyright (C) 2013 @ymmt2005, Cybozu

How to learn

公式マニュアルは作りかけで放置されていて役に立ちません。
具体的には、クラスひとつ作ることができないくらい

まとまった情報を得るには、以下の本くらいしかないのでこれを読みましょう。

でもだいぶ古くなっていたり、例外の投げ方が書いていなかったりするので、その辺りはもうソースを眺めるしかないです。
新設された API などはインストールされるヘッダファイルを眺めましょう。あと ext/jsonext/snmp 辺り
の公式拡張はコンパクトで up-to-date な感じなので参考にするといいです。

Summary of Extending and Embedding PHP

Chapter 1 The PHP Life Cycle

  • CLI・CGI
    1っ回こっきり
  • FastCGI / mod_php for prefork MPM
    シングルスレッド・複数リクエスト
  • mod_php for worker MPM
    マルチスレッド・複数リクエスト

に対応するため、MINIT/RINIT/RSHUTDOWN/MSHUTDOWN のライフサイクル管理用コールバックがある。

Chapter 2 Variables From the Inside Out

zval の型判定、C 型への取り出し、C 型から設定するマクロ集。HashTable の使い方もちょいと。
コピーやデストラクタは解説されてない。

MAKE_STD_ZVAL で作った zval を壊すのは zval_ptr_dtor らしい。
zval** を取るので注意。

void foo() {
    zval* zv;
    MAKE_STD_ZVAL(zv);
    ...
    zval_ptr_dtor(&zv); // &zv !
}

Chapter 3 Memory Management

malloc の代わりに emalloc 使え。persistent なら malloc でいいが、条件次第なら
pemalloc にフラグを渡して使え。integer overflow 防止には safe_emalloc を使え。

エラーを投げるのは php_error_docref 関数。

ZVAL_ADDREF でリファレンスカウントを増やせる。

あと PHP の「参照」がゴミだってことが書いてある。

Chapter 4 Setting Up a Build Environment

  • --enable-debug
    メモリリークを検出する
  • --enable-maintainer-zts
    ZTS を強制的に有効にしてビルドする

Chapter 5 Your First Extension

Function 追加の仕方。function_entry 配列末尾は PHP_FE_END マクロが使える。

Chapter 6 Returning Values

return_value という引数に入れて返す。RETURN_RESOURCE みたいなマクロも使える。

return_value_used という引数で、返り値が使われるかどうか判定できる。

ZEND_BEGIN_ARG_INFO マクロで引数の情報を設定してコンパイルタイムの参照渡しを実装する方法。

Chapter 7 Accepting Parameters

引数の数が違うときは WRONG_PARAM_COUNT マクロを使えばエラーになる(return するので注意)。

zend_parse_parameters には "p" も指定できて、ファイルパスかどうか検査できる。文字列の一種。

Arg Info and Type-hinting

type hinting から察せる通り、以下は結論としてほとんど役に立たない話

ZEND_BEGIN_ARG_INFO マクロで Zend engine に関数パラメーターの型チェックをやらせることができる。

例えば array を取る場合は

ZEND_BEGIN_ARG_INFO(php_hoge_arginfo, 0)  // struct name, pass_rest_by_reference
    ZEND_ARG_ARRAY_INFO(0, "arr", 0)      // pass_by_ref, name, allow_null
ZEND_END_ARG_INFO()

ないし

ZEND_BEGIN_ARG_INFO_EX(php_hoge_arginfo, 0, ZEND_RETURN_VALUE, 1)
    // struct name, pass_rest_by_reference, return_reference, required_num_args
    ZEND_ARG_ARRAY_INFO(0, "arr", 0)      // pass_by_ref, name, allow_null
ZEND_END_ARG_INFO()

array, object は専用マクロがあるが、その他は ZEND_ARG_TYPE_INFOIS_STRING とかを使うみたい。

#define ZEND_ARG_TYPE_INFO(pass_by_ref, name, type_hint, allow_null) \
    { #name, sizeof(#name)-1, NULL, 0, type_hint, allow_null, pass_by_ref},

と思ったら、ZEND_ARG_TYPE_INFOバグで使えないとのこと。
ZEND_ARG_INFO で緩く受けるしかなさそう。で、実際のところこれで型を指定しても、WARNING 止まりでエラーに
なってくれたりはしない。ぶっちゃけなくてもいいんじゃないかという。ゴミ。

Chapter 8 Working with Arrays and HashTables

飛ばし

HashTable は整数インデックスの配列としても、いわゆる連想配列としても使える。
Insane.

Chapter 9 The Resource Data Type

リソースは「リスト」という構造体に登録して int の ID を発行してもらって使う。
リストは MINIT でデストラクタを指定して作成する。

static int le_sample_resources;
PHP_MINIT_FUNCTION(sample) {
    le_sample_resources = zend_register_list_destructors_ex(
        /* dtor */, /* persistent dtor */, /* name */, module_number);
    return SUCCESS;
}

int の ID からリソースを取ってくるのは ZEND_FETCH_RESOURCE(2) マクロを使う。
このマクロはリソース取得に失敗したらエラー投げて RETURN_FALSE するようになっている。

Persistent resource

Persistent resource は通常のリソースとさして違いはない

  • メモリは pemalloc 系で allocate するべきこと
  • EG(persistent_list) ハッシュテーブルにキーと共に登録すること
  • 使う前に liveness チェックするのがいいよ

Reference counter for resources

zval とは別にリソースも参照カウントを持っている。zval が参照で増えたときに increment される。
まあ、普通には気にしなくていい。persistent resource であればリクエスト中は消えないわけで、
参照カウントが幾つであろうと、作り直す際には 1 にリセットして構わない。

Chapter 10 PHP4 Objects

レガシー。

getThis() は this ポインターを取るマクロ。インスタンスが取れないときは NULL が返る。

Chapter 11 PHP5 Objects

OOスタイルと関数スタイルで実装を共用する

本にも書いてある PHP_ME_MAPPING でマッピングするのに加えて、メソッドとして
呼ばれているかどうかで第一パラメーター "O" をスキップするか、検査するかを切り替える
zend_parse_method_parameters 関数で引数をパースする。(see ext/date)

OO だけなら通常通り zend_parse_parameters でパースして良い。

private / protected property

書いてある通りに実装しても、"Cannot access property..." というエラーになる。
デフォルトプロパティを定義して回避する Tips がここで紹介されている。

Inheritance

本に書いてないけど、zend_register_internal_class_ex のほうで親クラスを指定して登録できる。

Chapter 12 Startup, Shutdown, and a Few Points in Between

Constants

REGISTER_LONG_CONSTANT 等のマクロで定義する。
MINIT で定義するときは CONST_PERSISTENT フラグをつけて、RINIT ならつけない。
CONST_CS フラグはつけるものという理解でいい。

Extension Globals

アクセッサーマクロは ext_skel が自動生成するものを使えばいい。

本に書いてあるよりもいいやり方が最近はあるようだ。後述

Creating a skeleton

$ tar xjf php-5.4.17.tar.bz2
$ cd php-5.4.17/ext
$ ./ext_skel --extname=yrmcds
$ cd yrmcds

config.m4config.w32 をいじる。Linux だけでいいなら config.w32 は消す。

config.m4
dnl config.m4 for extension yrmcds

PHP_ARG_ENABLE(yrmcds, whether to enable yrmcds support,
[  --enable-yrmcds         Enable yrmcds support])

extra_sources="libyrmcds/close.c libyrmcds/connect.c libyrmcds/recv.c \
               libyrmcds/send.c libyrmcds/set_compression.c \
               libyrmcds/socket.c libyrmcds/strerror.c lz4/lz4.c"

if test "$PHP_YRMCDS" != "no"; then
    PHP_SUBST(YRMCDS_SHARED_LIBADD)
    PHP_NEW_EXTENSION(yrmcds, yrmcds.c $extra_sources, $ext_shared,,
                      "-D_GNU_SOURCE")
    PHP_ADD_INCLUDE([$ext_srcdir/libyrmcds])
fi

Building the new extension

autoconf とかは事前に準備してね。

$ cd php-5.4.17
$ ./buildconf --force
$ ./configure --enable-yrmcds --enable-debug (--enable-maintainer-zts)
  (so にするなら --enable-yrmcds=shared)
$ make

Globals

module globals の初期化は zend_module_entryPHP_GINIT(module) としてできるようになった。
jsonsession 等の既存のコードを参照のこと。

static PHP_GINIT_FUNCTION(ps) /* {{{ */
{
        ps_globals->save_path = NULL;
        ps_globals->session_name = NULL;
}

zend_module_entry session_module_entry = {
        STANDARD_MODULE_HEADER_EX,
        NULL,
        session_deps,
        "session",
        session_functions,
        PHP_MINIT(session),
        PHP_MSHUTDOWN(session),
        PHP_RINIT(session),
        PHP_RSHUTDOWN(session),
        PHP_MINFO(session),
        NO_VERSION_YET,
        PHP_MODULE_GLOBALS(ps),
        PHP_GINIT(ps),               // ここんとこ注目      
        NULL,
        NULL,
        STANDARD_MODULE_PROPERTIES_EX
};

Values

zval の union。Z_TYPE_P(zval*) == IS_NULL とかして型判定する。

Handlers

それぞれの型にキャストして扱うマクロ群

#define Z_LVAL(zval)                    (zval).value.lval
#define Z_BVAL(zval)                    ((zend_bool)(zval).value.lval)
#define Z_DVAL(zval)                    (zval).value.dval
#define Z_STRVAL(zval)                  (zval).value.str.val
#define Z_STRLEN(zval)                  (zval).value.str.len
#define Z_ARRVAL(zval)                  (zval).value.ht
#define Z_OBJVAL(zval)                  (zval).value.obj
#define Z_OBJ_HANDLE(zval)              Z_OBJVAL(zval).handle
#define Z_OBJ_HT(zval)                  Z_OBJVAL(zval).handlers
#define Z_OBJCE(zval)                   zend_get_class_entry(&(zval) TSRMLS_CC)
#define Z_OBJPROP(zval)                 Z_OBJ_HT((zval))->get_properties(&(zval) TSRMLS_CC)
#define Z_OBJ_HANDLER(zval, hf)         Z_OBJ_HT((zval))->hf
#define Z_RESVAL(zval)                  (zval).value.lval

Throwing an exception

../json/json.c:         zend_throw_exception_ex(NULL, 0 TSRMLS_CC, "Failed calling %s::jsonSerialize()", ce->name);

実際には SPL の RuntimeException を継承したりそのまま投げたりするコードが多いようだ。(see ext/pdo/pdo.c, ext/snmp/snmp.c)

Namespace

名前空間を扱うようのマクロが用意されている。内部的には単に ns + "\" + name にしているだけのようだ。

#define ZEND_NS_FENTRY(ns, zend_name, name, arg_info, flags) \
    ZEND_RAW_FENTRY(ZEND_NS_NAME(ns, #zend_name), name, arg_info, flags)
#define ZEND_NS_RAW_FENTRY(ns, zend_name, name, arg_info, flags) \
    ZEND_RAW_FENTRY(ZEND_NS_NAME(ns, zend_name), name, arg_info, flags)
#define ZEND_NS_RAW_NAMED_FE(ns, zend_name, name, arg_info) \
    ZEND_NS_RAW_FENTRY(ns, #zend_name, name, arg_info, 0)
#define ZEND_NS_NAMED_FE(ns, zend_name, name, arg_info) \
    ZEND_NS_FENTRY(ns, zend_name, name, arg_info, 0)
#define ZEND_NS_FE(ns, name, arg_info) \
    ZEND_NS_FENTRY(ns, name, ZEND_FN(name), arg_info, 0)
#define ZEND_NS_DEP_FE(ns, name, arg_info) \
    ZEND_NS_FENTRY(ns, name, ZEND_FN(name), arg_info, ZEND_ACC_DEPRECATED)
#define ZEND_NS_FALIAS(ns, name, alias, arg_info) \
    ZEND_NS_FENTRY(ns, name, ZEND_FN(alias), arg_info, 0)
#define ZEND_NS_DEP_FALIAS(ns, name, alias, arg_info) \
    ZEND_NS_FENTRY(ns, name, ZEND_FN(alias), arg_info, ZEND_ACC_DEPRECATED)

クラスを定義する際は、INIT_NS_CLASS_ENTRY マクロを使う。
定数定義には REGISTER_NS_LONG_CONSTANT 等のマクロを使う。
詳しくはこちら

PHP_METHOD, PHP_ME 等は名前空間対応マクロがないが、これらはローカルなシンボル
として定義してテーブルに関数ポインタを突っ込むだけの用途なので、名前空間を付加しなくても問題ない。
代わりに static PHP_METHOD(class_name, method_name)static 付けるのがお勧め。

Error & logging

php_error_docref 関数で PHP インタプリタのエラー割り込みを発生できる。
最終的には zend_error ないし zend_error_noreturn (noreturn attribute 付いただけ)が呼ばれる。

php_log_err 関数でエラーログを出力できる。

Test

test/*.phpt としてテストを作る。特定の拡張だけテストするには以下のようにする。

$ TEST_PHP_EXECUTABLE=/usr/local/php/bin/php ./run-tests.php ext/yrmcds