[Destripando PHP] Las variables

Con esta entrada comienzo una serie en la que hablaré del lenguaje PHP atendiendo a cómo está implementado internamente, para cada post desarrollaré un pequeño PECL (extensión de PHP) que trabaje los conceptos. En realidad no haría falta recurrir a ese nivel para ver las estructuras internas ya que PHP5 tiene un API de Reflection, pero sí que es necesario para ver realmente cómo funcionan las cosas.
La motivación de esta serie es ir documentando lo que voy aprendiendo en mi estudio de PHP. En el desarrollo de mi PFC me enganché al tema de los intérpretes y ahora trato de continuar tomando PHP como ejemplo, al mismo tiempo me ayuda a tener una base sólida de cara al ZCE.

En este primer post veremos en profundidad cómo se trabajan las variables en PHP y su idiosincrasia.

El PECL que he desarrollado para el mismo se llama “ejemplovars” y aporta las siguientes funciones:

  • ejemplovars_print_type($variable)
  • ejemplovars_print_refcount($variable)
  • ejemplovars_dump_globalsymtable
  • ejemplovars_dump_currentsymtable

Algunas cosas que veremos ya se trataron en otro post pero he preferido retomar el tema desde el principio.
.

El zval

PHP es un lenguaje de tipado débil, esto implica que se realizan un montón de conversiones implícitas entre los tipos de valores hasta el punto que desde el punto de vista del programador PHP puede dar la impresión de que en lugar de haber tipos las cosas funcionan mágicamente.

Los datos en php se almacenan en una estructura de datos llamada “zval” (Zend Value). Podemos encontrar esta estructura en “Zend/Zend.h”.

struct _zval_struct {
        /* Variable information */
        zvalue_value value;
        zend_uint refcount;
        zend_uchar type;
        zend_uchar is_ref;
};

Podemos ver que un zval está compuesto por 4 miembros: value, refcount, type y is_ref. refcount es un unsigned integer mientras que type y is_ref son unsigned char.
El miembro “value” sin embargo es una estructura union. La diferencia entre un struct y una union es que sólo ocupa lo que necesite el miembro más grande de la union, en lugar de reservarse memoria dedicada a cada uno de los miembros.
Tiene perfecto sentido por tanto que esta estructura de datos sea una union ya que un “value” sólo puede ser de un tipo cada vez, usar un struct sería un gran desaprovechamiento.

Los tipos

Según la guía de Zend los tipos de datos en PHP son:

  • boolean
  • int
  • float
  • string
  • array
  • object
  • NULL
  • resource

Sin embargo, internamente los tipos son los siguientes (constantes numéricas):

  • IS_NULL: se asigna automáticamente a las variables no inicializadas. La asignación se realiza en php con la palabra reservada NULL. Hay que tener en cuenta que NULL no es lo mismo que FALSE ni que cero, desde php lo parece debido a los casting implícitos que realiza.
  • IS_BOOL: sólo puede tomar los valores TRUE y FALSE. Cuando se evalúan expresiones como WHILE o IF, lo que se hace es un casting implícito a ese tipo. Es decir, como en NULL, no se trata de que 0 sea lo mismo que FALSE sino que ese es un casting que se realiza automáticamente.
  • IS_LONG: los int en php son internamente “signed long”, dependiente de la plataforma (en 32bit el rango es menor que en 64bit). Si en un script php una variable int se sale del rango, se convierte automáticamente al tipo que veremos a continuación.
  • IS_DOUBLE: para números de coma flotante se utiliza el tipo “signed double” del sistema. Esto supone que la precisión no es exacta. Se entiende con este ejemplo :
    
    

    Cuando la precisión es una factor crítico se recomienda el uso de BCMath.

  • IS_STRING: cadena de caracteres, se reserva un bloque de memoria para acomodar el string en cuestión, guardando en el zval un puntero al mismo. La longitud del string se guarda también en el zval. Esto permite que el string contenga datos binarios ya que un “” no lo va a truncar dado que se maneja la longitud independientemente. Debido a esta práctica se dice que los string de php son “binary safe”. El bloque de memoria reservado es siempre la longitud del string + 1 ya que se añade “” al final para que el string se pueda trabajar desde cualquier función sin tener que depender de la longitud guardada en el zval.
  • IS_ARRAY: un array en php en realidad no cumple con la definición clásica de array, sino que se trata de un mapa ordenado o tabla. Esto hace que muchos programadores que sólo conocen PHP se lleven una gran sorpresa al enfrentarse a otros lenguajes y ver que los arrays son mucho más limitados que en php. Quizá habría sido más afortunado llamar a este tipo collection o map directamente, pero a efectos prácticos es irrelevante. Aclarado esto, un array de php está implementado con una tabla Hash donde la clave (o label) puede ser un índice numérico o un string asociativo y está relacionada con un valor (o data).

    Internamente también se sigue esa generalización y se usa el tipo HashTable en lugar de usar arrays o listas enlazadas. Supongo que habrán considerado que la flexibilidad y facilidad de uso que supone compensa con creces el impacto que pueda tener en cuanto a eficiencia. El tipo HashTable se encuentra definido en :

    typedef struct _hashtable {
            uint nTableSize;
            uint nTableMask;
            uint nNumOfElements;
            ulong nNextFreeElement;
            Bucket *pInternalPointer;       /* Used for element traversal */
            Bucket *pListHead;
            Bucket *pListTail;
            Bucket **arBuckets;
            dtor_func_t pDestructor;
            zend_bool persistent;
            unsigned char nApplyCount;
            zend_bool bApplyProtection;
    #if ZEND_DEBUG
            int inconsistent;
    #endif
    } HashTable;
  • IS_OBJECT: Un objeto de php viene a ser un array (de php) al que se le añaden métodos, modificadores de acceso y constantes. El modelo de objetos cambió radicalmente de PHP4 a PHP5.
  • IS_RESOURCE: se trata de un tipo especial utilizado para casos como un puntero FILE o una conexión a base de datos.

Probando con nuestro PECL

Un ejemplo con nuestro PECL:

attr = 25;
ejemplovars_print_type($a);
$a = 4;
ejemplovars_print_type($a);
$a = $a * 1000000000;
ejemplovars_print_type($a);

El resultado en una máquina de 32bits será:

Tipo: IS_BOOL
Tipo : IS_OBJECT
Tipo: IS_LONG
Tipo : IS_DOUBLE
<?php
$a = 5;
$b = &$a;
$c = &$a;
ejemplovars_print_refcount($a);
ejemplovars_print_type($a);

El resultado será:

Referenciado 4 veces.
Tipo: IS_LONG

El código de las dos funciones de ejemplovars es el siguiente, he evitado utilizar macros para trabajar con el zval para que quede claro el acceso a la estructura. Está probado en PHP 5.2, para 5.3 es probable que sea necesario algún cambio ya que para el nuevo recolector de basura han tocado el struct zval. La función ejemplovars_print_type usa paso por valor mientras que ejemplovars_print_refcount usa paso por referencia implícito.

PHP_FUNCTION(ejemplovars_print_refcount)
{
    zval *arg;
    if (zend_get_parameters(ZEND_NUM_ARGS(), 1, &arg) == FAILURE) {
        RETURN_NULL();
    }
    if (!arg->is_ref) {
        return;
    }

    php_printf("Referenciado %d veces.\n", arg->refcount);
}
PHP_FUNCTION(ejemplovars_print_type)
{
   zval *arg;
   if (zend_get_parameters(ZEND_NUM_ARGS(), 1, &arg) == FAILURE) {
      php_error_docref(NULL TSRMLS_CC, E_WARNING, "Falta el parámetro.");
      RETURN_NULL();
   }
   switch (arg->type) {
      case IS_NULL:
         php_printf("Tipo: IS_NULL\n");
         break;
      case IS_BOOL:
         php_printf("Tipo: IS_BOOL\n");
         break;
      case IS_LONG:
         php_printf("Tipo: IS_LONG\n");
         break;
      case IS_DOUBLE:
         php_printf("Tipo : IS_DOUBLE\n");
         break;
      case IS_STRING:
         php_printf("Tipo : IS_STRING\n");
         break;
      case IS_ARRAY:
         php_printf("Tipo : IS_ARRAY\n");
         break;
      case IS_OBJECT:
         php_printf("Tipo : IS_OBJECT\n");
         break;
      case IS_RESOURCE:
         php_printf("Tipo: IS_RESOURCE\n");
         break;
      default:
         php_printf("Otro tipo\n");
   }

}

Las tablas de símbolos

Una tabla de símbolos es un HashTable con punteros a zvals. Hay una tabla de símbolos global que se crea al comienzo, y que podemos ver desde nuestros scripts php : $GLOBALS.

<?php
$a = "lala";

function prueba() {
   $b = "jaja";
   print_r($GLOBALS);
}

prueba();

Veremos que la variable $b no figura en esa tabla de símbolos. Esto es así ya que al entrar en nuevos ámbitos locales, ya sea por entrar en una función o en el método de una clase, se crea una nueva tabla de símbolos y se establece esta como la tabla de símbolos activa. Es decir, en cada momento sólo tendremos acceso a dos tablas de símbolos: la global y la del ámbito concreto en que estemos. Al terminar la vida de una función su tabla de símbolos se destruye. Podemos caer en el error de pensar que entonces sólo va a haber dos tablas de símbolo en memoria como mucho, pero repito: se crea una por cada nuevo ámbito. Si desde una función o método llamas a otro, ya tienes otra tabla de símbolos iniciada sólo que la de la función invocadora no está activa. Con este diseño es fácil entender cómo funciona el concepto de visibilidad de variables locales.

La tabla de símbolos global así como el puntero a las demás se guarda en la estructura _zend_executor_globals. A continuación un fragmento, si quieres verla entera la encontrarás en zend_globals.h.

        /* symbol table cache */
        HashTable *symtable_cache[SYMTABLE_CACHE_SIZE];
        HashTable **symtable_cache_limit;
        HashTable **symtable_cache_ptr;

        zend_op **opline_ptr;

        HashTable *active_symbol_table;
        HashTable symbol_table;         /* main symbol table */

        HashTable included_files;       /* files already included */

        jmp_buf *bailout;

        int error_reporting;
        int orig_error_reporting;
        int exit_status;

        zend_op_array *active_op_array;

        HashTable *function_table;      /* function symbol table */
        HashTable *class_table;         /* class table */
        HashTable *zend_constants;      /* constants table */

Podemos acceder fácilmente a la estructura zend_globals con la macro EG. ejemplovars tiene dos funciones que acceden a la tabla de símbolos global y la activa respectivamente:

PHP_FUNCTION(ejemplovars_dump_globalsymtable)
{
      php_printf("Tabla de simbolos global : \n\n");
      php_ejemplovars_print_var_hash(&(EG(symbol_table)));
}

PHP_FUNCTION(ejemplovars_dump_currentsymtable)
{
      php_printf("Tabla de simbolos actualmente activa : \n\n");
      php_ejemplovars_print_var_hash(EG(active_symbol_table));
}

La tabla de símbolos global se pasa por referencia ya que zend_executor_globals la incluye directamente en lugar de simplemente un puntero a la misma.

Tabla de simbolos global :

The value of GLOBALS is: Array
The value of _ENV is: Array
The value of HTTP_ENV_VARS is: Array
The value of argv is: Array
The value of argc is: 1
The value of _POST is: Array
The value of HTTP_POST_VARS is: Array
The value of _GET is: Array
The value of HTTP_GET_VARS is: Array
The value of _COOKIE is: Array
The value of HTTP_COOKIE_VARS is: Array
The value of _SERVER is: Array
The value of HTTP_SERVER_VARS is: Array
The value of _FILES is: Array
The value of HTTP_POST_FILES is: Array
The value of _REQUEST is: Array
The value of a is: lala

Tabla de simbolos actualmente activa :

The value of b is: jaja

Próximamente subiré “ejemplovars” a github o similar.

Si te interesa el tema de PECL recomiendo el libro “Extending and Embedding PHP” de Sara Golemon.

Published by

Iván Mosquera Paulo

Software Engineer

2 thoughts on “[Destripando PHP] Las variables”

  1. Ese arraintxo! 😛

    Pues esto es un hola mundo y se entienden bastante bien algunas partes de PHP, es ir tirando del hilo.

Leave a reply to arraintxo Cancel reply