イテレーター

先ほどのセクションでは、型付き配列のPHPの言語への統合が上手くいくように、いくつかオブジェクトハンドラーを実装しました。しかしまだ、考慮しなければならないことがあり、それはイテレーションです。このセクションでは、PHPの内部でどのようにイテレーションが実装されているかや、それをどのように利用できるかといった点をみていきたいと思います。ここでも例として、型付き配列を使用していきます。

get_iteratorハンドラー

PHPの内部でのイテレーションの動作は、ユーザーランドでの IteratorAggregate インターフェイスと非常によく似ています。このクラスは zend_object_iterator* を返す get_iterator ハンドラーを持っており、以下のようになっています。

struct _zend_object_iterator {
    void *data;
    zend_object_iterator_funcs *funcs;
    ulong index; /* fe_reset/fe_fetch オペコードでのみ使用 */
};

index メンバーは内部的に foreach の実装に使用されています。これが各イテレーションの度にインクリメントされ、もし独自のキー関数を指定しなければ、キーとして使用されます。 funcs メンバーにはそれぞれのイテレーションでの動作のためのハンドラーが含まれています。

typedef struct _zend_object_iterator_funcs {
    /* このイテレーターインスタンスに関連する全てのリソースを解放する */
    void (*dtor)(zend_object_iterator *iter TSRMLS_DC);

    /* イテレーションの最後かどうかをチェックする (データが妥当であれば SUCCESSを、そうでなければFAILUREを返す) */
    int (*valid)(zend_object_iterator *iter TSRMLS_DC);

    /* 現在の要素からデータを取得する */
    void (*get_current_data)(zend_object_iterator *iter, zval ***data TSRMLS_DC);

    /* 現在の要素からキーを取得する (オプション。 NULLかもしれない) */
    void (*get_current_key)(zend_object_iterator *iter, zval *key TSRMLS_DC);

    /* 次の要素にすすむ */
    void (*move_forward)(zend_object_iterator *iter TSRMLS_DC);

    /* 最初の要素に巻き戻す (オプション。 NULLかもしれない) */
    void (*rewind)(zend_object_iterator *iter TSRMLS_DC);

    /* 現在の値やキーを無効にする (オプション。 NULLかもしれない) */
    void (*invalidate_current)(zend_object_iterator *iter TSRMLS_DC);
} zend_object_iterator_funcs;

このハンドラーは Iterator インターフェイスのものとそれぞれ名前が僅かに違うだけで、非常によく似ています。唯一、 invalidate_current はユーザーランドの方に対応するものがありませんが、これは現在のキー/値を破棄するために使用されます。しかしこれはほとんど使われておらず、特に foreach からもこれが呼び出されることすらありません。

イテレーターの構造体の最後に紹介するメンバーは data で、独自のデータを持ちまわることができます。大抵、1スロットでは十分でないので、そういう時は、 zend_object での場合と同様に構造体を拡張します。

型付き配列をイテレートするために、いくつかのデータを保持しておかなければなりません。まず最初に、バッファビューオブジェクトへの参照を持っておく必要があります(さもないと、イテレーションの間に破棄されるかもしれません)。その参照は data メンバーで保持しておくとよいでしょう。さらに、 buffer_view_object をハンドラーが呼び出される度に取得しなおさなくてもよいように、手元に置いておく必要があります。また、現在のイテレーションの offset と、現在の値の zval* を保持しておく必要があります(これが必要な理由は後ほど説明します)。

typedef struct _buffer_view_iterator {
    zend_object_iterator intern;
    buffer_view_object *view;
    size_t offset;
    zval *current;
} buffer_view_iterator;

では準備が整ったので、例として zend_object_iterator_funcs 構造体を宣言してみましょう。

static zend_object_iterator_funcs buffer_view_iterator_funcs = {
    buffer_view_iterator_dtor,
    buffer_view_iterator_valid,
    buffer_view_iterator_get_current_data,
    buffer_view_iterator_get_current_key,
    buffer_view_iterator_move_forward,
    buffer_view_iterator_rewind
};

では get_iterator ハンドラーを実装していきましょう。このハンドラーはクラスエントリとそのオブジェクト、イテレーションが参照でおこなわれているかどうかのフラグを受け取って、 zend_object_iterator* を返します。ここで必要になってくるのは、イテレーターのメモリ割り当てとそれぞれのメンバーの初期化をおこなうということだけです。

zend_object_iterator *buffer_view_get_iterator(
    zend_class_entry *ce, zval *object, int by_ref TSRMLS_DC
) {
    buffer_view_iterator *iter;

    if (by_ref) {
        zend_throw_exception(NULL, "Cannot iterate buffer view by reference", 0 TSRMLS_CC);
        return NULL;
    }

    iter = emalloc(sizeof(buffer_view_iterator));
    iter->intern.funcs = &buffer_view_iterator_funcs;

    iter->intern.data = object;
    Z_ADDREF_P(object);

    iter->view = zend_object_store_get_object(object TSRMLS_CC);
    iter->offset = 0;
    iter->current = NULL;

    return (zend_object_iterator *) iter;
}

最後に、バッファビュークラスの登録のためにマクロを調整しなければなりません。

#define DEFINE_ARRAY_BUFFER_VIEW_CLASS(class_name, type)                     \
    INIT_CLASS_ENTRY(tmp_ce, #class_name, array_buffer_view_functions);      \
    type##_array_ce = zend_register_internal_class(&tmp_ce TSRMLS_CC);       \
    type##_array_ce->create_object = array_buffer_view_create_object;        \
    type##_array_ce->get_iterator = buffer_view_get_iterator;                \
    type##_array_ce->iterator_funcs.funcs = &buffer_view_iterator_funcs;     \
    zend_class_implements(type##_array_ce TSRMLS_CC, 2,                      \
        zend_ce_arrayaccess, zend_ce_traversable);

これまでと比べて、Traversable の実装を設定している点と、 get_iteratoriterator_funcs.funcs への代入部分が加わりました。

イテレーター関数

では上記で記述している buffer_view_iterator_funcs を実際に実装していきましょう。

static void buffer_view_iterator_dtor(zend_object_iterator *intern TSRMLS_DC)
{
    buffer_view_iterator *iter = (buffer_view_iterator *) intern;

    if (iter->current) {
        zval_ptr_dtor(&iter->current);
    }

    zval_ptr_dtor((zval **) &intern->data);
    efree(iter);
}

static int buffer_view_iterator_valid(zend_object_iterator *intern TSRMLS_DC)
{
    buffer_view_iterator *iter = (buffer_view_iterator *) intern;

    return iter->offset < iter->view->length ? SUCCESS : FAILURE;
}

static void buffer_view_iterator_get_current_data(
    zend_object_iterator *intern, zval ***data TSRMLS_DC
) {
    buffer_view_iterator *iter = (buffer_view_iterator *) intern;

    if (iter->current) {
        zval_ptr_dtor(&iter->current);
    }

    if (iter->offset < iter->view->length) {
        iter->current = buffer_view_offset_get(iter->view, iter->offset);
        *data = &iter->current;
    } else {
        *data = NULL;
    }
}

#if ZEND_MODULE_API_NO >= 20121212
static void buffer_view_iterator_get_current_key(
    zend_object_iterator *intern, zval *key TSRMLS_DC
) {
    buffer_view_iterator *iter = (buffer_view_iterator *) intern;
    ZVAL_LONG(key, iter->offset);
}
#else
static int buffer_view_iterator_get_current_key(
    zend_object_iterator *intern, char **str_key, uint *str_key_len, ulong *int_key TSRMLS_DC
) {
    buffer_view_iterator *iter = (buffer_view_iterator *) intern;

    *int_key = (ulong) iter->offset;
    return HASH_KEY_IS_LONG;
}
#endif

static void buffer_view_iterator_move_forward(zend_object_iterator *intern TSRMLS_DC)
{
    buffer_view_iterator *iter = (buffer_view_iterator *) intern;

    iter->offset++;
}

static void buffer_view_iterator_rewind(zend_object_iterator *intern TSRMLS_DC)
{
    buffer_view_iterator *iter = (buffer_view_iterator *) iter;

    iter->offset = 0;
    iter->current = NULL;
}

それぞれの関数はかなり単純なので、簡単な説明にとどめます。

get_current_datazval*** data をパラメーターとして受け取り、それに *data = ... とすることで zval** を書き込むことを想定しています。イテレーションは参照渡しの可能性もあり、その場合は zval* だと十分でないので zval** でなければなりません。 イテレーターで currentのメンバーで zval* を保持しておかなければならないのは、このように ``zval** を扱うからです。

get_current_key の実装はPHPのバージョンに依存しています。PHP5.5では単に受け取った key の変数に ZVAL_* マクロのいづれかを使ってキーを書き込むだけです。

5.5以前のPHPでの get_current_key ハンドラーは3つのパラメーターを受け取り、これは返すキーの型に対応してセットされます。もし HASH_KEY_NON_EXISTANT を返す場合には、結果としてのキーは null となり、パラメーターには何もセットする必要がありません。 HASH_KEY_IS_LONG の場合には、 int_key をセットします。 HASH_KEY_IS_STRING の場合には、 `str_keystr_key_len をセットしなければなりません。ここでの str_key_len は( zend_hash APIでの場合と同様に)文字列の長さに1を足したものであることに注意してください。

継承について

ここでもユーザーがクラスを継承してイテレーションの振る舞いを変えたいと思う場合について考えなければなりません。現状では個別のイテレーションのハンドラーがユーザーランドからは隠蔽されているので、継承した際にはユーザーがイテレーションの仕組みを手動で再実装しなければなりません。

オブジェクトハンドラーで既におこなってきたように、通常の Iterator インターフェイスを実装するというかたちで解決します。今回は、PHPが実際にオーバーライドしたメソッドを呼び出すのを保証するために、特別なハンドリングは必要ありません。というのも、PHPはクラスが継承されておらず直接使用されている場合には最初の内部のハンドラーを使用しますが、クラスが拡張されている場合は Iterator のメソッドを使用するからです。

Iterator メソッドを実装するためには、buffer_view_objectsize_t current_offset メンバーを追加して、イテレーションメソッドのために現在のオフセットを保持するようにしなければなりません(そして get_iterator のイテレーターで使われるイテレーションの状態とは完全に分離します)。メソッドの実装それ自体は、お決まりの引数のチェック処理が大半です。

PHP_FUNCTION(array_buffer_view_rewind)
{
    buffer_view_object *intern;

    if (zend_parse_parameters_none() == FAILURE) {
        return;
    }

    intern = zend_object_store_get_object(getThis() TSRMLS_CC);
    intern->current_offset = 0;
}

PHP_FUNCTION(array_buffer_view_next)
{
    buffer_view_object *intern;

    if (zend_parse_parameters_none() == FAILURE) {
        return;
    }

    intern = zend_object_store_get_object(getThis() TSRMLS_CC);
    intern->current_offset++;
}

PHP_FUNCTION(array_buffer_view_valid)
{
    buffer_view_object *intern;

    if (zend_parse_parameters_none() == FAILURE) {
        return;
    }

    intern = zend_object_store_get_object(getThis() TSRMLS_CC);
    RETURN_BOOL(intern->current_offset < intern->length);
}

PHP_FUNCTION(array_buffer_view_key)
{
    buffer_view_object *intern;

    if (zend_parse_parameters_none() == FAILURE) {
        return;
    }

    intern = zend_object_store_get_object(getThis() TSRMLS_CC);
    RETURN_LONG((long) intern->current_offset);
}

PHP_FUNCTION(array_buffer_view_current)
{
    buffer_view_object *intern;
    zval *value;

    if (zend_parse_parameters_none() == FAILURE) {
        return;
    }

    intern = zend_object_store_get_object(getThis() TSRMLS_CC);
    value = buffer_view_offset_get(intern, intern->current_offset);
    RETURN_ZVAL(value, 1, 1);
}

/* ... */

ZEND_BEGIN_ARG_INFO_EX(arginfo_buffer_view_void, 0, 0, 0)
ZEND_END_ARG_INFO()

/* ... */

PHP_ME_MAPPING(rewind, array_buffer_view_rewind, arginfo_buffer_view_void, ZEND_ACC_PUBLIC)
PHP_ME_MAPPING(next, array_buffer_view_next, arginfo_buffer_view_void, ZEND_ACC_PUBLIC)
PHP_ME_MAPPING(valid, array_buffer_view_valid, arginfo_buffer_view_void, ZEND_ACC_PUBLIC)
PHP_ME_MAPPING(key, array_buffer_view_key, arginfo_buffer_view_void, ZEND_ACC_PUBLIC)
PHP_ME_MAPPING(current, array_buffer_view_current, arginfo_buffer_view_void, ZEND_ACC_PUBLIC)

当然ですが、 Traversable ではなく Iterator の実装を設定しなければなりません。

#define DEFINE_ARRAY_BUFFER_VIEW_CLASS(class_name, type)                     \
    INIT_CLASS_ENTRY(tmp_ce, #class_name, array_buffer_view_functions);      \
    type##_array_ce = zend_register_internal_class(&tmp_ce TSRMLS_CC);       \
    type##_array_ce->create_object = array_buffer_view_create_object;        \
    type##_array_ce->get_iterator = buffer_view_get_iterator;                \
    type##_array_ce->iterator_funcs.funcs = &buffer_view_iterator_funcs;     \
    zend_class_implements(type##_array_ce TSRMLS_CC, 2,                      \
        zend_ce_arrayaccess, zend_ce_iterator);

ここで最後に注意しなければならないことがあります。一般的には、 Iterator よりも IteratorAggregate を実装する方が良いでしょう。なぜなら IteratorAggregate だとイテレーターの状態をメインのオブジェクトから分離できるからです。このデザイン設計が良いのは明らかで、ネストしてもそれぞれを独立したイテレーションとして振る舞う事も可能となります。それでもここで Iterator を選んでいるのは IteratorAggregate の方が(独立したオブジェクトとやり取りをおこなう別々のクラスが必要となってくるために)実装上のオーバーヘッドが大きいからです。