If anybody feels like explaining something else that's also puzzling about the Zend engine and PHP arrays; I had a few hours spent the other day on a WTF moment writing a PHP extension in C and querying array zvals.
I was doing something fairly simple, trying to extract values passed as named argument to a function and turning them back into simple C types (char * and int):
The part that I still don't understand (but that I figured out by trial-and-error) was why `zend_hash_find` takes a `void••`[1] as argument, which should actually be a `zval•••` cast as `void••`. What's the purpose of the triple pointer here?
Here's my understanding of why each pointer is needed, from reading the Zend source code for a while:
1) The innermost pointer is needed because Zend hash tables actually store a "zval* " (pointer to zval), not a zval directly. The zval is allocated separately, then its pointer is stored into the table.
2) The second pointer is needed because Zend tables internally malloc storage for whatever they store (zval* ) in this case, then access that data as a pointer. The "zval* " pointer is memcpy'd into the malloc'd area. This data is accessed through a "zval* * " pointer. This allows users of zend_hash_xxx to not only access the "zval* " pointer, but also change it.
3) In C, one way for a function to return a value is by passing a pointer to a variable that will store the result. Since zend_hash_find returns the internal "zval* * " data, you need to pass in a "zval* * * " pointer to a "zval* * " pointer that is the actual return value you want. Through this "zval* * " pointer, you can read and also change the "zval* " data stored in that hash table cell.
1) Symbol tables store double pointer, not single, see my comment to the parent for the reason why. There may be hash tables that store single pointers, but not symbol tables.
PHP arrays store zval ••. The reason for it has to do with references - basically, if you want to do $a =& $b; and then $a = 1 and want for $b be 1 now too, if symbol table for b is storing zval •, there's no way to change it when $a = 1 is executed. However, if b's entry stores pointer to actual zval •, and so does a's entry, when you change that storage to point to zval with 1 instead, both $a and $b would change. Hope I explain it clearly, hard to do it without drawing a picture :)
So, to receive zval ••, you need to pass zval ••• to the hash function. That's why the type in general is void ••, because generic hash (hashes are used for all kinds of things, not only zvals) stores void •, so to receive it you pass void ••.
Just for fun, there are places in the code IIRC where quadruple pointer can be found (see zend_fcall_info_args_save for example). Pretty rare case though, don't remember any place with five-times pointer.
I was doing something fairly simple, trying to extract values passed as named argument to a function and turning them back into simple C types (char * and int):
The part that I still don't understand (but that I figured out by trial-and-error) was why `zend_hash_find` takes a `void••`[1] as argument, which should actually be a `zval•••` cast as `void••`. What's the purpose of the triple pointer here? [1]: Imagine the • there is a star / asterisk.