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);