エクステンションでforeachを簡単に書くには: イテレータ対応版

zval_foreach.h (ヘッダのみ)
zval_foreach_example.tgz (CodeGen_PECLで生成したコードとサンプルスクリプトつき)


IoでPHPイテレータに対してforeachさせたかったので、イテレータ用のITER_FOREACHマクロを作成しました。マクロの内容はこのようになっています。(※行末のエスケープ(バックスラッシュ)は省略しています)
全体的にはHASH_FOREACHと似ていますね。HASH_FOREACHでは第一引数がHashTable *だったのが、ITER_FOREACHではzend_object_iterator *です。また、PHPでのrewind(), key()相当のC関数はNULLでもよいことになっているのでそのチェックをしているのと、key(), current()の後に例外を捕捉している点、キー文字列がコピーとして代入されるので開放している点も異なります。

#define ITER_FOREACH(_it, _key, _value, _code) {
	long _idx = 0L;

	if (_it->funcs->rewind != NULL) {
		_it->funcs->rewind(_it TSRMLS_CC);
	}

	while (_it->funcs->valid(_it TSRMLS_CC) == SUCCESS) {
		zval **_data = NULL;
		zstr  _kstr;
		uint  _klen = 0U;
		ulong _knum = 0UL;
		zval _kzv, *_key, *_value;

		INIT_ZVAL(_kzv);
		_key = &_kzv;

		if (_it->funcs->get_current_key == NULL) {
			ZVAL_LONG(_key, _idx++);
		} else {
			switch (_it->funcs->get_current_key(_it, &_kstr, &_klen, &_knum TSRMLS_CC)) {
			  case HASH_KEY_IS_UNICODE:
				ZVAL_UNICODEL(_key, _kstr.u, _klen - 1, 0);
				break;
			  case HASH_KEY_IS_STRING:
				ZVAL_STRINGL(_key, _kstr.s, _klen - 1, 0);
				break;
			  case HASH_KEY_IS_LONG:
				ZVAL_LONG(_key, (long)_knum);
				break;
			  default:
				ZVAL_NULL(_key);
			}
		}

		_it->funcs->get_current_data(_it, &_data TSRMLS_CC);
		_value = *_data;

		if (EG(exception) != NULL) {
			break;
		}

		if (Z_TYPE(_kzv) != IS_NULL) {
			_code;
		}

		zval_dtor(&_kzv);

		_it->funcs->move_forward(_it TSRMLS_CC);
	}
}


さらにHASH_FOREACHとITER_FOREACHをラップするZVAL_FOREACHマクロも作成。_resultにはforeachの成否が代入されるint型の変数、_zvにはzval *を指定します。
PHP5/6:

#if (PHP_MAJOR_VERSION == 5) && (PHP_MINOR_VERSION < 2)
#define FOREACH_GETITER_BY_REF_CC
#else
#define FOREACH_GETITER_BY_REF_CC , 0
#endif
#define ZVAL_FOREACH(_result, _zv, _key, _value, _code) {
	HashTable *_ht = NULL;
	_result = FAILURE;

	if (Z_TYPE_P(_zv) == IS_OBJECT) {
		zend_class_entry *_ce = Z_OBJCE_P(_zv);

		if (_ce->get_iterator != NULL) {
			zend_object_iterator *_it = _ce->get_iterator(_ce, _zv FOREACH_GETITER_BY_REF_CC TSRMLS_CC);

			if (_it != NULL) {
				if (EG(exception) == NULL) {
					ITER_FOREACH(_it, _key, _value, _code);
					_result = SUCCESS;
				}
				_it->funcs->dtor(_it TSRMLS_CC);
			}
		} else if (Z_OBJ_HT_P(_zv)->get_properties != NULL) {
			_ht = Z_OBJPROP_P(_zv);
		}
	} else if (Z_TYPE_P(_zv) == IS_ARRAY) {
		_ht = Z_ARRVAL_P(_zv);
	}

	if (_ht != NULL) {
		HASH_FOREACH(_ht, _key, _value, _code);
		_result = SUCCESS;
	}
}

PHP4:

#define ZVAL_FOREACH(_result, _zv, _key, _value, _code) {
	HashTable *_ht = HASH_OF(_zv);
	_result = FAILURE;

	if (_ht != NULL) {
		HASH_FOREACH(_ht, _key, _value, _code);
		_result = SUCCESS;
	}
}


CodeGen_PECL用のspecファイルで使用例を示します。key, valueは一時的な変数なので、必要に応じてコピーしなければいけません。
foreach.xml

<?xml version="1.0"?>
<extension name="foreach" version="1.0.0">
<code position="top"><![CDATA[
#include "zval_foreach.h"
]]></code>
<function name="foreach_println">
<proto>void foreach_println(mixed iter)</proto>
<code><![CDATA[
int result;

ZVAL_FOREACH(result, iter, key, value,
	zval str_value = *value;
	zval_copy_ctor(&str_value);
	convert_to_string(&str_value);

	switch (Z_TYPE_P(key)) {
	  case IS_LONG:
		php_printf("%ld => %s\n", Z_LVAL_P(key), Z_STRVAL(str_value));
		break;
	  case IS_STRING:
		php_printf("%s => %s\n", Z_STRVAL_P(key), Z_STRVAL(str_value));
		break;
#ifdef IS_UNICODE
	  case IS_UNICODE:
		{
			char *ascii_key = zend_unicode_to_ascii(Z_USTRVAL_P(key), Z_USTRLEN_P(key) TSRMLS_CC);
			php_printf("%s => %s\n", ascii_key, Z_STRVAL(str_value));
			efree(ascii_key);
		}
		break;
#endif
	}

	zval_dtor(&str_value);
);

if (result == FAILURE) {
	php_error(E_WARNING, "an array, a plain object or an iterator expected, but %s given",
			zend_zval_type_name(iter));
}
]]></code>
</function>
</extension>

上記エクステンションのテスト用コード。最後の foreach_println($str); ではE_WARNINGが発生し、引数の型名はPHP6でunicode.semantics=Onの場合は "Unicode string"、それ以外は "string" と表示されます。

<?php
dl('foreach.so');

class TestIterator implements Iterator {
    private $n = 0;
    public function rewind()  { $this->n = 0; }
    public function next()    { $this->n++; }
    public function valid()   { return $this->n < 10; }
    public function key()     { return chr(ord('a') + $this->n); }
    public function current() { return str_repeat('+', $this->n + 1); }
}

$arr = array(1, 2, 3);

$obj = new stdClass();
$obj->foo = 'bar';
$obj->baz = 'qux';

$iter = new TestIterator();

$str = 'Hello, Foreach!';

foreach_println($arr);
foreach_println($obj);
foreach_println($iter);
foreach_println($str);