I work for a company that is using php and would like to
contribute a few helper functions that we find useful.
They are most helpful for anyone doing OO work (especially
if using singletons) who isn't ready to move to php5.
Two of the three are trivial to incorporate, but
for the third I'm looking for some advice.  I'm also not
sure if I should get a cvs account for this stuff or
if posting it here is sufficient inspiration for some
good natured soul to get the functions into php. :)
I'll start with the easy ones:

============ ref_id(mixed var) ============

ref_id returns a value that is the same for any variables
that are references to eachother, but different for any
variables that are not references to eachother.  In the
file system analogy, this corresponds to doing an "ls -i".
In some cases it is useful to check if an object really
is the object it is expected to be.  A forgotten "&" can
lead to hours of debugging that can be reduced with
appropriate ref_id checks.  Here is an example:

<?php
        $x = 5;
        $y = 7;
        $z =& $x;
        if (ref_id($x) == ref_id($y)) {
                print "Changing x or y changes the other.\n";
        }
        if (ref_id($x) == ref_id($z)) {
                print "Changing x or z changes the other.\n";
        }
?>

causes the output:

Changing x or z changes the other.

Here's the code:

/* {{{ proto int ref_id(mixed var)
 *    Returns the pointer of the variable */
PHP_FUNCTION(ref_id)
{
        zval **arg;

        if (ZEND_NUM_ARGS() != 1 ||
            zend_get_parameters_ex(1, &arg) == FAILURE) {
                WRONG_PARAM_COUNT;
        }

        RETURN_LONG((int)*arg);
}
/* }}} */

PHP_FE(ref_id, first_arg_force_ref)

============ ref_count(mixed var) ============

ref_count returns the number of references for the given
variable.  In the file system analogy, this corresponds to
the second column of the output of "ls -l".
In some cases it is useful to check if an object really
is the object it is expected to be.  A forgotten "&" can
lead to hours of debugging that can be reduced with
appropriate ref_count checks.  Here is an example:

<?php
        function f($x, $y) {
                if (ref_count($x) > 1) {
                        print "Changing x affects another var.\n";
                } else {
                        print "x is strictly local.\n";
                }
                if (ref_count($y) > 1) {
                        print "Changing y affects another var.\n";
                } else {
                        print "y is strictly local.\n";
                }
        }
        $x = 5;
        $y = 7;
        f($x, &$y);
?>

causes the output:

x is strictly local.
Changing y affects another var.

(Ordinarily, the "&" would appear in the parameter
list to ensure a reference is passed, but in certain
situations it is preferable to place the "&" in
the calling code.)

Here's the code:

/* {{{ proto int ref_count(mixed var)
 *    Returns the number of references to the variable */
PHP_FUNCTION(ref_count)
{
        zval **arg;

        if (ZEND_NUM_ARGS() != 1 ||
            zend_get_parameters_ex(1, &arg) == FAILURE) {
                WRONG_PARAM_COUNT;
        }

        /* "-1" because a reference is created just to call this
function */
        RETURN_LONG(ZVAL_REFCOUNT(*arg)-1);
}
/* }}} */

PHP_FE(ref_count, first_arg_force_ref)

============ (dis)allow_copies(mixed var) ============

When the design calls for a singleton, the design calls
for an asserting copy constructor.  It can take hours to
discover that the reason a singleton is returning
strange values is that there are accidentally multiple
instances of the singleton class.  The bug may be a
missing "&" in what should be "$singleton =& getSingleton();"
or the bug may be a missing "&" in the declaration or
calling of a function.  The ill effects are often seen
in a largely unrelated piece of code.  In C++, the
answer is to declare a copy constructor and leave it
unimplemented.  In php, disallow_copies($x) makes the
following code an error: "$y = $x;".  Likewise "f($x)"
where f does not take a reference would be an error.
For completeness, allow_copies($x) returns a variable to
"normal", ie "$y = $x" is OK again.

Here is the code, but see below for a major issue.

/* {{{ proto void allow_copies(mixed var)
 *    Causes php to not generate an error if the script later tries to
copy
 *    the variable */
PHP_FUNCTION(allow_copies)
{
        zval **arg;

        if (ZEND_NUM_ARGS() != 1 ||
            zend_get_parameters_ex(1, &arg) == FAILURE) {
                WRONG_PARAM_COUNT;
        }
        ZVAL_ALLOW_COPIES(*arg) = 1;
}
/* }}} */

PHP_FE(allow_copies, first_arg_force_ref)

/* {{{ proto void disallow_copies(mixed var)
 *    Causes php to generate an error if the script later tries to copy
 *    the variable */
PHP_FUNCTION(disallow_copies)
{
        zval **arg;

        if (ZEND_NUM_ARGS() != 1 ||
            zend_get_parameters_ex(1, &arg) == FAILURE) {
                WRONG_PARAM_COUNT;
        }
        ZVAL_ALLOW_COPIES(*arg) = 0;
}
/* }}} */

PHP_FE(disallow_copies, first_arg_force_ref)

In zend.h:

struct _zval_struct {
        /* Variable information */
        zvalue_value value;             /* value */
        zend_uchar type;        /* active type */
        zend_uchar is_ref;
        zend_ushort refcount;
        zend_bool allow_copies;      /* NEW NEW NEW */
};

#define ZVAL_ALLOW_COPIES(pz)   ((pz)->allow_copies)

#define INIT_PZVAL(z)           \
        (z)->allow_copies = 1;  /* NEW NEW NEW */        \
        (z)->refcount = 1;              \
        (z)->is_ref = 0;

In zend.c:

        zval_used_for_init.is_ref = 0;
        zval_used_for_init.refcount = 1;
        zval_used_for_init.allow_copies = 1;   /* NEW NEW NEW */
        zval_used_for_init.type = IS_NULL;

In zend_variable.c:

ZEND_API int _zval_copy_ctor(zval *zvalue ZEND_FILE_LINE_DC)
{
        /* This is a notice instead of error because of the issue
           discussed below */
        if (!zvalue->allow_copies) {
                zend_error(E_NOTICE, "Cannot copy value for which
allow_copies is false");
        }
        ...

Whew!  This email is a lot longer than I realized it would be
and I'm only just now getting to the problem.  Hopefully, at
least a couple of people are reading this part.  "In theory,"
the code above should be a complete implementation of the
feature.  In practice, I found that a lot of the code in php
doesn't initialize the variables used.  That may be fine for a
particular piece of code that only relies on a couple of members
of the struct, but it makes it hard to add functionality later.
Here's an example from my notes of part of my effort to fix this:

Replace any call to ALLOC_ZVAL(z) that is not immediately followed by
   a line of the form "*z = whatever" or INIT_PZVAL(z) with
ALLOC_INIT_ZVAL(z).
   vi `find . -name "*.[hc]" | xargs grep -l 'ALLOC_ZVAL'`

That helped and so did initializing the 5 cases of bare
zend_constant variables and the many bare znode variables,
but eventually I gave up tracking down every last piece of
code that lacked initialization.  If I was on the tip with
a cvs account, I would have at least felt it was worthwhile.
(Who knows; maybe uninitialized values are contributing to
bugs in php...)  As it was, it was a throw-away effort.  My
boss suggested keeping a separate table instead of adding
to the zval struct and just ignoring the cases in which
an unitialized zval is looked for in the table.  We agreed
that isn't as clean and has worse performance.  We also
agreed that the best course is to write to the php development
community for guidance.

So... any thoughts?

Thanks!
Todd Ruth

-- 
PHP Internals - PHP Runtime Development Mailing List
To unsubscribe, visit: http://www.php.net/unsub.php

Reply via email to