今回の記事でも GitHub における php/php-src の commit c5d10ddda394b573dfaea1380285e1dd5f3c0d50 を前提としている.
前回は HashTable 構造体には nNumOfElements というメンバがあり, PHP の count 関数ではその値を読んでいることがわかった.
つまり, 配列の要素数が変わるタイミングで, nNumOfElements の値は書き変わるはずだ.
今回はその辺りに付いて読んでみる.
./Zend/zend_hash.c の中に, 以下のようなマクロ, 関数を見つけた.
_zend_hash_init のパラメータの最後についている ZEND_FILE_LINE_DC はソースコードのファイル名, 行数を引数として渡すマクロのようだ.
デバッグ時のみ渡されるようになるが, zend_hash_init マクロを呼ぶ限りは, そういったことは意識する必要が無い.
./Zend/zend.h で定義されている.
zend_hash_init はその名前からすると, HashTable の初期化に使用するものだと思われる.
nTableSize メンバには HashTable の大きさと思われる値が代入されており, 0x80000000 は超えないようにされている.
そのあとはパラメータや定数をそのまま代入しているだけだ.
ただし, pHashFunction は特にどのメンバにもセットされておらず, 使われていないようだ.
zend_hash_init が呼ばれている箇所を grep してみても, どれも NULL を渡しているようだ.
配列の初期化時点では空なので, 当然 nNumOfElements は 0 だ.
nNextFreeElement も 0 となっているが, これは PHP で $arr[] = “foo” などとしたときに暗黙的に指定されるキーだろうか.
また, pListHead や pListTail のような, リスト要素を指すメンバにも当然のごとく NULL で初期化されている.
そして, pInternalPointer は, ./Zend/zend_hash.h の HashTable 構造体を定義している箇所のコメントによると, Used for element traversal とされている.
恐らく foreach などで array を走査するときに, 現在の要素を保持しておくためのものだろうか.
これらのメンバが配列操作時にどのように扱われているか, 見ていこう.
./Zend/zend_hash.c に _zend_hash_index_update_or_next_insert という関数がある.
いかにも $arr[0] = “foo” といった操作時に呼ばれてそうだ.
grep してもあまりヒットしないが, ./Zend/zend_hash.h 上にこのようなマクロがあった.
zend_hash_index_update も zend_hash_next_index_insert も _zend_hash_index_update_or_next_insert を呼んでおり, それぞれのマクロは以下の点において違うようだ.
- zend_hash_next_index_insert では関数に渡す第二引数の指定はできず, 常に 0 が渡るようになっている.
- フラグとして HASH_UPDATE か HASH_NEXT_INSERT が渡されている.
_zend_hash_index_update_or_next_insert 関数を見てみよう.
日本語のコメントは全て私が書き込んだものだ.
HashTable の arBuckets という配列に, 新しく生成した値をセットしつつ, 双方向リストとしても整合を取るよう, CONNECT_TO_BUCKET_DLLIST マクロを呼んでいる.
CONNECT_TO_BUCKET_DLLIST は ./Zend/zend_hash.c で定義されている.
異常から大体以下のようなことがわかった.
- HashTable はその要素として Bucket 構造体を指すポインタの配列を持つ.
- Bucket は双方向リストとして各要素が連結されている.
- HashTable を追加したタイミングで, その要素数や, 次の空き要素の位置が更新される.
次は Bucket 構造体についてでも読んでみようと思う.