Archivo por días: 2 agosto, 2009

Vala de plata

Hace un par de meses descubrí la existencia de Vala, un nuevo lenguaje de programación similar a C# pero con una característica muy interesante: en lugar de compilar directamente a código máquina o a código de una máquina virtual, compila a código C, usando GObject para implementar el sistema de clases y objetos. La gran ventaja de  esto es que permite crear bibliotecas y clases GObject sin necesidad de lidiar con la complejidad de este sistema, lo que, para los fans de Gnome nos resulta especialmente atractivo. También permite trabajar en un lenguaje moderno, lo suficientemente parecido a C# como para que casi no se note la diferencia, pero sin perder ni un ápice de rendimiento (lo siento, sigo sin creerme que un JIT consiga siempre mejores rendimientos, fuera de casos patológicos  y pruebas de laboratorio).

Otro detalle que me ha parecido fascinante es que simplifica mucho la gestión de memoria. Al contrario de lo que podría parecer, no utiliza un recolector de basura como el de Java, sino conteo de referencias. Lo interesante es que todo el proceso es muy transparente, pues el propio lenguaje se encarga de utilizarlo casi siempre. Así, un trozo de código como éste:

using GLib;

class cPrueba:Object {

}

int main() {

    cPrueba miObjeto,miObjeto2;

    miObjeto=new cPrueba();
    stdout.printf("Referencias: %udn",miObjeto.ref_count);
    miObjeto2=miObjeto;
    stdout.printf("Referencias: %ud %udn",miObjeto.ref_count,miObjeto2.ref_count);
    miObjeto=new cPrueba();
    stdout.printf("Referencias: %ud %udn",miObjeto.ref_count,miObjeto2.ref_count);
    return 0;
}

cuando se ejecuta, devuelve la siguiente salida por pantalla:

Referencias: 1d
Referencias: 2d 2d
Referencias: 1d 1d

Vemos que, cuando se hace la primera asignación, se crea un nuevo objeto de tipo cPrueba y se asigna a miObjeto, con lo que su contador de referencias es 1. Luego hacemos la segunda asignación. Como miObjeto y miObjeto2 son, realmente, punteros al mismo objeto (y apuntan, por tanto, a la misma zona de memoria), lo único que hace Vala es incrementar en uno el contador de referencias; por eso los contadores de referencias de miObjeto y miObjeto2 valen 2 en la segunda línea de la salida: porque en realidad son el mismo objeto, pero está referenciado por dos punteros.

Cuando, finalmente, asignamos un nuevo objeto a miObjeto, primero Vala libera el objeto al que apunta, lo que en la práctica consiste en decrementar su contador de referencias. Si en ese momento éste pasase a valer cero, entonces ese objeto quedaría huérfano (ningún puntero apunta a él), por lo que Vala procedería a destruirlo automáticamente, liberando su memoria; sin embargo, en este ejemplo eso no ocurre porque miObjeto2 está apuntando al objeto viejo, así que lo único que ocurre es que su contador de referencias pasa a valer 1. Por su parte, se crea un nuevo objeto de tipo cPrueba, apuntado por miObjeto, cuyo contador de referencias también vale 1, como vemos en la tercera línea de la salida.

Si observamos el código fuente generado (usando valac -C prueba.vala) su función MAIN queda como:

gint _main (void) {
    cPrueba* miObjeto;
    cPrueba* miObjeto2;
    cPrueba* _tmp0;
    cPrueba* _tmp2;
    cPrueba* _tmp1;
    cPrueba* _tmp3;
    gint _tmp4;
    miObjeto = NULL;
    miObjeto2 = NULL;
    _tmp0 = NULL;
    miObjeto = (_tmp0 = cprueba_new (), (miObjeto == NULL) ? NULL : (miObjeto = (g_object_unref (miObjeto), NULL)), _tmp0);
    fprintf (stdout, "Referencias: %udn", ((GObject*) miObjeto)->ref_count);
    _tmp2 = NULL;
    _tmp1 = NULL;
    miObjeto2 = (_tmp2 = (_tmp1 = miObjeto, (_tmp1 == NULL) ? NULL : g_object_ref (_tmp1)), (miObjeto2 == NULL) ? NULL : (miObjeto2 = (g_object_unref (miObjeto2), NULL)), _tmp2);
    fprintf (stdout, "Referencias: %ud %udn", ((GObject*) miObjeto)->ref_count, ((GObject*) miObjeto2)->ref_count);
    _tmp3 = NULL;
    miObjeto = (_tmp3 = cprueba_new (), (miObjeto == NULL) ? NULL : (miObjeto = (g_object_unref (miObjeto), NULL)), _tmp3);
    fprintf (stdout, "Referencias: %ud %udn", ((GObject*) miObjeto)->ref_count, ((GObject*) miObjeto2)->ref_count);
    return (_tmp4 = 0, (miObjeto == NULL) ? NULL : (miObjeto = (g_object_unref (miObjeto), NULL)), (miObjeto2 == NULL) ? NULL : (miObjeto2 = (g_object_unref (miObjeto2), NULL)), _tmp4);
}

(He omitido toda la parte de la definición GObject de la clase ejemplo porque no aporta nada). Vemos en las distintas líneas como cada vez que se hace una asignación de un objeto a una variable, primero se procede a liberar lo que hubiese en dicha variable utilizando una llamada a g_object_unref, para asegurarse de que el contador de referencias decrece correctamente. Luego llama a g_object_ref con la variable que se copia para incrementar su contador de referencias. Como vemos, este proceso automático nos simplifica la vida lo suficiente como para que nos podamos olvidar de la gestión automática de la memoria…

O casi, porque existe, al menos, un caso en el que no se cumple todo ésto: las listas. Sin embargo, tras buscar y rebuscar, encontré que es un bug en Vala, así que ya está reportado (http://bugzilla.gnome.org/show_bug.cgi?id=586577) y debería estar corregido en breve, asi que no añadais una llamada a unref a mano, sino simplemente esperad a que lo corrijan y recompilad para eliminar el memory leak.

Existe, de todas formas, una opción alternativa, que es utilizar LibGee y su ArrayList. Este tipo de listas no tiene el bug que comento, por lo que, si se utiliza, no hay riesgo de memory leaks.

Pese a todo, es importante señalar un par de detalles sobre las listas de Glib en Vala: si vemos la documentacion, hay varias formas de declararlas, en concreto:

  • var milista = List<contenido_de_lista>();
  • List<contenido_de_lista> milista = List<contenido_de_lista>();
  • List milista = List<contenido_de_lista>();

Siendo contenido_de_lista una clase de elementos que contendra la lista. Asi, si quiero hacer una lista de strings la definiria como List<string>.  La cuestion es que, en base a las pruebas que he hecho, solo se realiza gestion de memoria si la lista se define como en la primera o la segunda opcion, porque solo ahi el compilador podra estar seguro de que el contenido es una clase derivada de Object y tiene, por tanto, contador de referencias. Si definimos la lista como en el tercer caso no se realizara gestion de memoria, por lo que podriamos incluso encontrarnos con que metemos en una lista global un elemento definido localmente y que, al terminar la funcion en donde se definio dicho elemento y volver a la funcion principal, dicho elemento es liberado, consiguiendo un hermoso core dump en cuanto intentemos acceder a dicho dato en la lista. Por tanto, mi consejo es utilizar siempre listas bien definidas, salvo que se sepa perfectamente lo que se esta haciendo.