実例で学ぶPHP拡張モジュールの作り方

第7回クラスの実装(前編)

今回から数回にわたって、第4回までに作ったcvcapture拡張モジュールを改造していきます。まずは単純なクラスを実装してみましょう。

おさらいとクラス設計

これまでのcvcapture拡張モジュールにはカメラまたは動画ファイルからリソースを作成する関数cv_create_camera_capture()、cv_create_camera_capture()と、リソースを引数にとってキャプチャした画像を保存する関数cv_create_camera_capture()があります。

これを以下のPHPコードのようなイメージでクラス化してみましょう。

リスト1 CvCaptureクラスのプロトタイプ
class CvCapture
{
    const ANY        = CV_CAP_ANY;
    /* 中略 */
    const PVAPI      = CV_CAP_PVAPI;

    private $capture;

    private function __construct($capture)
    {
        $this->capture = $capture;
    }

    public function save($filename, &$size = null)
    {
        return cv_save_capture($this->capture, $filename, $size);
    }

    static public function createCameraCapture($index = self::ANY)
    {
        $capture = cv_create_camera_capture($index);
        if (is_resource($capture)) {
            return new CvCapture($capture);
        }
        return false;
    }

    static public function createFileCapture($filename)
    {
        $capture = cv_create_file_capture($filename);
        if (is_resource($capture)) {
            return new CvCapture($capture);
        }
        return false;
    }
}

specファイル定義

リスト1のクラスをCodeGen_PECLのspecファイルで表すには、リスト2のように、ルート要素である<extension>の中に<class>要素、その中に<constant>、<property>、<function>を配置します。

リスト2 specファイルよりクラス定義を抜粋
<?xml version="1.0" encoding="UTF-8"?>
<extension name="cvcapture" version="0.3.0">
  <!-- リソースやグローバル定数・関数の定義は省略 -->
  <class name="CvCapture">
    <constant type="int" name="ANY" value="CV_CAP_ANY">autodetect</constant>
    <!-- 中略 -->
    <constant type="int" name="PVAPI" value="CV_CAP_PVAPI">PvAPI, Prosilica GigE SDK</constant>

    <property name="capture" access="private">cvcapture resource</property>

    <function access="public" static="yes" name="createCameraCapture">
      <proto>object createCameraCapture([int index])</proto>
    </function>
    <function access="public" static="yes" name="createFileCapture">
      <proto>object createFileCapture(string filename)</proto>
    </function>
    <function access="public" name="save">
      <proto>bool save(string filename[, mixed &amp;size])</proto>
    </function>
  </class>
</extension>

グローバル定数は<constants>要素の中に<constant>を配置しましたが、クラス定数の場合は<class>の中に直接<constant>を配置します。

プロパティは<property>要素で指定します。ここではname属性とaccess属性だけを使っていますが、type属性とvalue属性でデフォルト値を指定することもできます。

メソッドはグローバル関数と同じ<function>要素で指定しますが、access属性でアクセシビリティを指定できるほか、abstract="yes"、final="yes"、static="yes"の指定もできます。また、abstract/final属性はクラスに、static属性はプロパティにもつけられます。

なお、リスト1のプロトタイプでは便宜上コンストラクタを書いていますが、CvCaptureクラスではcreateCameraCapture()/createFileCapture()の中でコンストラクタ相当の処理もするように実装する計画なのでspecファイルにはコンストラクタを書いていません。

コンストラクタを定義したい場合は、単にメソッド名を⁠__construct⁠にするだけで、特別な指定は必要ありません[1]⁠。

ソースコードを生成する

ではこれまで通りpecl-genコマンドでソースコードを生成しましょう[2]⁠。

操作1 pecl-genを実行
$ pecl-gen --dir=cvcapture-0.3.0 cvcapture-0.3.0.xml
Creating 'cvcapture' extension in 'cvcapture-0.3.0'

Your extension has been created in directory ./cvcapture-0.3.0.
See ./cvcapture-0.3.0/README and/or ./cvcapture-0.3.0/INSTALL for further instructions.

今回は動作するコードを実装する前に、生成されたソースコードcvcapture.cからCvCaptureクラスに関係する箇所を先頭から見ていきましょう。

ソースコードを読む:クラス情報

まずは59行目に以下のような記述がありました。

リスト3 zend_class_entry *(cvcapture.c:59行目)
static zend_class_entry * CvCapture_ce_ptr = NULL;

zend_class_entryはPHPのクラスを定義する構造体で、CvCapture_ce_ptrにはCvCaptureクラスの情報が格納されます。ここでいったんPHP本体のソースコードからzvalの定義を見直してみましょう。

リスト4 zval(php-5.3.x/Zend/zend.h)
/*
 * zval
 */
typedef struct _zval_struct zval;
typedef struct _zend_class_entry zend_class_entry;

typedef struct _zend_guard {
        zend_bool in_get;
        zend_bool in_set;
        zend_bool in_unset;
        zend_bool in_isset;
        zend_bool dummy; /* sizeof(zend_guard) must not be equal to sizeof(void*) */
} zend_guard;

typedef struct _zend_object {
        zend_class_entry *ce;
        HashTable *properties;
        HashTable *guards; /* protects from __get/__set ... recursion */
} zend_object;

#include "zend_object_handlers.h"

typedef union _zvalue_value {
        long lval;                                      /* long value */
        double dval;                            /* double value */
        struct {
                char *val;
                int len;
        } str;
        HashTable *ht;                          /* hash table value */
        zend_object_value obj;
} zvalue_value;

struct _zval_struct {
        /* Variable information */
        zvalue_value value;             /* value */
        zend_uint refcount__gc;
        zend_uchar type;        /* active type */
        zend_uchar is_ref__gc;
};

zval.value.obj.ceにzend_class_entry *が格納されています。つまり、 zval.type = IS_OBJECT かつ zval.value.obj.ce = CvCapture_ce_ptr; な値がCvCaptureオブジェクトとなるのです。

ソースコードを読む:メソッドのひな形

さらに読み進めましょう。クラスエントリー宣言のすぐ下ではメソッドのひな形が生成されています。

リスト5 メソッドのひな形(cvcapture.c:66行目)
PHP_METHOD(CvCapture, createCameraCapture)
{
        zend_class_entry * _this_ce;

        zval * _this_zval = NULL;
        long index = 0;



        if (zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC, getThis(), "O|l", &_this_zval, CvCapture_ce_ptr, &index) == FAILURE) {
                return;
        }

        _this_ce = Z_OBJCE_P(_this_zval);


        php_error(E_WARNING, "createCameraCapture: not yet implemented"); RETURN_FALSE;

        object_init(return_value)
}

関数はPHP_FUNCTION(name)マクロで定義されていましたが、メソッドはPHP_METHOD(classname, name)マクロで定義されます[3]⁠。

ここで出てきたgetThis()はZend/zend_API.hで定義されているマクロで、関数がインスタンスメソッドの場合に呼び出し元のオブジェクトが代入されます。ただしcreateCameraCaptureはスタティックメソッドなのでこの値はNULLになります。

関数の場合は引数の取得にzend_parse_parameters()を使っていましたが、メソッドではzend_parse_method_parameters()を使うようになっています。

これはdate拡張モジュールの各関数のように、オブジェクト指向APIと手続き型APIの両方をもつ関数の実装を簡略化するために使われるのがほとんどの冗長な表現です。しかも、この場合はgetThis() == NULLなので意味がありません。

さらにobject_init()の後にセミコロンがなく、確実にコンパイルに失敗するので、リスト5はリスト6のように修正する必要があります。また、この後に続くcreateFileCapture()のひな形にも同様に修正します。

リスト6 CvCapture::createCameraCapture()のひな形を修正
PHP_METHOD(CvCapture, createCameraCapture)
{
        long index = 0;

        if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "l", &index) == FAILURE) {
                return;
        }

        php_error(E_WARNING, "createCameraCapture: not yet implemented"); RETURN_FALSE;

        object_init_ex(return_value, CvCapture_ce_ptr);
}

object_init(return_value)では戻り値をstdClassとして初期化するだけなので、object_init_ex(return_value, CvCapture_ce_ptr)に変更、CvCaptureクラスのインスタンスとして初期化しています。

まだカメラに接続してリソースをプロパティに代入する処理が書かれていませんが、それは次回に実装します。

ソースコードを読む:メソッド情報

その他のメソッドのひな形の後、それらをまとめたzend_function_entry構造体の配列があります。

リスト7 CvCaptureクラスのメソッド情報(cvcapture.c:144行目)
static zend_function_entry CvCapture_methods[] = {
        PHP_ME(CvCapture, createCameraCapture, CvCapture__createCameraCapture_args, /**/ZEND_ACC_PUBLIC | ZEND_ACC_STATIC)
        PHP_ME(CvCapture, createFileCapture, CvCapture__createFileCapture_args, /**/ZEND_ACC_PUBLIC | ZEND_ACC_STATIC)
        PHP_ME(CvCapture, save, CvCapture__save_args, /**/ZEND_ACC_PUBLIC)
        { NULL, NULL, NULL }
};

PHP_ME()はメソッド情報を記述しやすくするための関数型マクロで、クラス名、メソッド名、引数情報[4]⁠、アクセシビリティのフラグを引数に取ります。

ここで配列なのに区切りのカンマが無いことに気付かれた方は鋭いです。PHP_MEを含むzend_function_entryのためのマクロはカンマ込みで定義されているので、カンマは不要というか、むしろ付けてはいけないのです[5]⁠。

最後の { NULL, NULL, NULL } はこれ以上エントリがないことを示す終端記号で、char[]を '\0' で終端するのと同じく、お約束ごとです。

ソースコードを読む:クラス登録

関数情報の次はいよいよ実際にクラスを有効にする処理です。

リスト8 クラスを登録する関数(cvcapture.c:153行目)
static void class_init_CvCapture(void)
{
        zend_class_entry ce;

        INIT_CLASS_ENTRY(ce, "CvCapture", CvCapture_methods);
        CvCapture_ce_ptr = zend_register_internal_class(&ce);

        /* {{{ Property registration */

        zend_declare_property_null(CvCapture_ce_ptr, 
                "capture", 7, 
                ZEND_ACC_PRIVATE TSRMLS_DC);

        /* }}} Property registration */


        /* {{{ Constant registration */

        do {
                zval *tmp, *val;
                zend_declare_class_constant_long(CvCapture_ce_ptr, "ANY", 3, CV_CAP_ANY TSRMLS_CC );
                /* 中略 */
                zend_declare_class_constant_long(CvCapture_ce_ptr, "PVAPI", 5, CV_CAP_PVAPI TSRMLS_CC );
        } while(0);

        /* } Constant registration */

}

まずINIT_CLASS_ENTRY()でローカル変数のzend_class_entry構造体ceを初期化、次にzend_register_internal_class()でceの内容をクラスのシンボルテーブルに登録しています。このときceの内容はemalloc()で確保された領域にコピーされ、CvCapture_ce_ptrにはそのポインタが代入されます。そしてそこにプロパティやクラス定数を登録しています[6]⁠。

この関数はもう少し下に書かれているPHP_MINIT_FUNCTION(cvcapture)でモジュール初期化時に呼び出されます。

ビルドしてみよう

CvCaptureクラスに関係するコードの解説はこれで終わりです。メソッド未実装の状態ですが、お約束の手順でビルドしてみましょう。

操作2 拡張モジュールをビルドする
$ phpize
$ ./configure
$ make

大抵の環境ではそのまま通って操作3のように出力されるのですが、PHPがスレッドセーフの場合は操作4のようなエラーが出てしまいます。

操作3 ビルド成功したときの出力
Build complete.
Don't forget to run 'make test'.
操作4 スレッドセーフなPHPではコンパイルエラー
/../cvcapture.c: In function ‘class_init_CvCapture’:
/../cvcapture.c:142: error: too few arguments to function ‘zend_register_internal_class’
/../cvcapture.c:148: error: expected expression before ‘void’
/../cvcapture.c:157: error: ‘tsrm_ls’ undeclared (first use in this function)
/../cvcapture.c:157: error: (Each undeclared identifier is reported only once
/../cvcapture.c:157: error: for each function it appears in.)
make: *** [cvcapture.lo] Error 1

これもやはりCodeGen_PECLが生成するコードに問題があるためのエラーで、下記のような修正が必要となります。

リスト9 ZTS対応パッチ
--- cvcapture.c.orig
+++ cvcapture.c
@@ -134,18 +134,18 @@
 
 /* }}} Methods */
 
-static void class_init_CvCapture(void)
+static void class_init_CvCapture(TSRMLS_D)
 {
         zend_class_entry ce;
 
         INIT_CLASS_ENTRY(ce, "CvCapture", CvCapture_methods);
-        CvCapture_ce_ptr = zend_register_internal_class(&ce);
+        CvCapture_ce_ptr = zend_register_internal_class(&ce TSRMLS_CC);
 
         /* {{{ Property registration */
 
         zend_declare_property_null(CvCapture_ce_ptr, 
                 "capture", 7, 
-                ZEND_ACC_PRIVATE TSRMLS_DC);
+                ZEND_ACC_PRIVATE TSRMLS_CC);
 
         /* }}} Property registration */
 
@@ -238,7 +238,7 @@
         REGISTER_LONG_CONSTANT("CV_CAP_PVAPI", CV_CAP_PVAPI, CONST_PERSISTENT | CONST_CS);
         le_cvcapture = zend_register_list_destructors_ex(cvcapture_dtor, 
                                                    NULL, "cvcapture", module_number);
-        class_init_CvCapture();
+        class_init_CvCapture(TSRMLS_C);
 
         /* add your stuff here */

これでスレッドセーフなPHP向けにもコンパイルできるようになります。

まとめと次回予告

今回はPHPのクラスを定義するためのspecファイルの書き方と、それがC言語ではどのようなコードになるのかを紹介しました。

肝心のメソッドが未実装のままですので、次回はcvcapture.cを編集して実際に動くクラスを作成します。

また、CodeGen_PECLはひな形生成には大変便利なのですが、生成するソースコードの品質が少し残念なことが分かりました。

今回までで通常の拡張モジュール作成に必要なspecファイルの書き方は大体説明できたので、これから制作する拡張モジュールはCodeGen_PECLを使ってひな形を生成したコードであっても、PHP/Zend EngineのAPIやTipsに絞って解説していきたいと思います。

サンプルファイルのダウンロード

おすすめ記事

記事・ニュース一覧