Archive for the 'Programación' Category

Simplificando Vala y CMake

viernes, abril 5th, 2013

He estado tres semanas currando sin parar en mi trabajo-que-paga-las-facturas, y aunque no pude disfrutar ni findes ni vacaciones (¡Gracias, gracias, ministros que pusieron la fecha de entrega de los innterconecta para justo después de semana santa! ¡Así os salga un cardo en el sobaco!), sí pude aprovechar algunos ratos muertos en casita para hacer un proyectillo personal: Autovala.

La idea detrás de Autovala surgió de casualidad de una lista de correo: un usuario se quejaba de que trabajar con CMake era muy pesado, y que obligar a usarlo para los proyectos de la lista sólo conseguía reducir la cantidad de desarrolladores que estarían dispuestos a colaborar. Y la verdad es que tiene algo de razón: empezar un proyecto nuevo con CMake es una tarea realmente aburrida, por la sencilla razón de que es muy repetitiva.

Pero cuando una tarea es repetitiva… es ideal para que la haga una máquina. Así que mi idea fue: ¿por qué no dejar que sea el propio ordenador quien genere de manera automática los ficheros CMake?

Y esa es, precisamente, la idea detrás de Autovala: generar de manera automática todos los ficheros para CMake, en base a una serie de heurísticos. Así, por defecto, todos los ficheros .vala que haya en el directorio src (o subdirectorios dentro de él) se compilarán juntos para hacer un único binario. Además, añadirá automáticamente los paquetes necesarios para cada fichero, para lo que echa un vistazo a las directivas using del propio código. Lo mismo para otros elementos: por ejemplo, para los iconos comprobará qué tamaño tienen y, en base a ello, decidirá automáticamente en qué carpeta deben ir. Y más, mucho más.

Por supuesto, es posible añadir excepciones a mano para ajustar aquellas cosas en las que las reglas no funcionen bien; por ejemplo, si lo que queremos es generar una biblioteca, o si nuestro proyecto está compuesto de varios ejecutables. La ventaja en estos casos es que sólo hay que hacer eso: indicarle que queremos que sean dos, tres, o los que sean, ejecutables y donde buscar las fuentes; autovala seguirá haciendo el resto automáticamente (buscar paquetes necesarios, etc). Además, los cambios que haga el usuario se recuerdan, por lo que si se añaden o eliminan ficheros, basta con recrear el fichero de proyecto, y se tendrán en cuenta todas las excepciones hechas antes.

Obviamente Autovala no sirve para todos los proyectos, pero sí para la gran mayoría, que es lo interesante.

Subtitulando

jueves, noviembre 15th, 2012

Estos días estoy liado buscando alternativas a periscope para bajar subtítulos de manera automática. El motivo es que no es un proyecto que esté siendo mantenido de manera razonable.

De casualidad encontré subliminal, un programa similar pero con más fuentes de subtítulos, y decidí echarle un ojo. La verdad es que funciona muy bien, pero tiene el inconveniente de que, al igual que periscope, está escrito en python, lo que significa que hay que meter una máquina virtual completa en mi WebTV.

Pero de casualidad encontré submarine, otro programa similar pero escrito en Vala, lo que significa que puedo compilarlo en un ejecutable y ahorrarme todo el interprete de python. Por desgracia, este sólo soporta búsquedas en OpenSubtitles.org y en Podnapisi.net. Sin embargo, su modelo interno es tan sumamente sencillo que he conseguido añadir soporte para SubDB (el cual ya está subido a mi repositorio GIT personal y solicitado un commit al oficial).

Extraoficialmente también he conseguido implementar soporte para BierDopje, pero ese código todavía no lo puedo subir porque sigo a la espera de que me concedan una clave específica para la API (en estos momentos estoy usando prestada la de subliminal, pero cada aplicación tiene que usar la suya propia).

El uso de este portal tiene su complicación, en buena medida porque mientras que SubDB utiliza un hash del archivo para buscar subtítulos, BierDopje pide el nombre de la serie, la temporada y el capítulo. En subliminal se utiliza la impresionante biblioteca guessit, que, a partir del nombre del fichero, y mediante una serie de heurísticos, extrae toda la información posible sobre un fichero. Obviamente dicha biblioteca está escrita en python, así que tuve que hacer una versión de andar por casa en Vala. El concepto básico es el mismo (de hecho, fusilé todo lo que pude), pero, al estar hecha en una noche, no es tan precisa. Obviamente acepto parches para mejorarla.

Funciones asíncronas en Vala

jueves, agosto 30th, 2012

Actualizado. Acabo de modificar un poco el código de Cronopete para que haga uso de las funciones asíncronas de Vala. El motivo es eliminar, en la medida de lo posible, el uso de threads.

Las funciones asíncronas de Vala son una ayuda muy interesante a la hora de realizar tareas que requieren mucho tiempo, a la vez que se quiere evitar que la interfaz se bloquee. La solución más inmediata en estos casos consiste en crear un thread y realizar en él todo el proceso, pero en ocasiones es una solución muy compleja, pues se necesita intercambiar datos entre éste y el thread principal, o sincronizar ambos. La solución está en las funciones asíncronas. Estas son funciones que pueden interrumpirse y reanudarse a voluntad en cualquier punto, lo que permite realizar una forma de multitarea cooperativa de manera sencilla.

Imaginemos que definimos una función o método asíncronos así:

public async int AsyncFnc(int p1, int p2, out int p3, out String p4) {
    // código de la función asíncrona (también puede ser un método de una clase)
    [...]
}

Para llamar a esta función, lo haremos de esta manera:

AsyncFnc.begin(p1, p2, callback_finalizacion);

Vemos que sólo incluimos los parámetros de entrada, pero no los de salida. Por otro lado, tampoco recogemos el valor devuelto. Por último, tenemos un misterioso callback_finalizacion. Este parámetro es opcional, y sólo hay que añadirlo cuando se desea ejecutar algún código cuando la función asíncrona finalice. Es en dicha función donde recogeremos los valores de salida y cualquier excepción que se produjese durante la ejecución de la función asíncrona. Dicho callback es de tipo GLib.AsyncReadyCallback, y su formato es el siguiente:

public delegate void AsyncReadyCallback (Object? source_object, AsyncResult res)

Sin embargo, Vala nos ofrece algunas ventajas, como son las funciones anónimas y los closures, que nos dan como ventaja el poder acceder desde el callback a variables externas a éstas. Así pues, utilizando una función anónima podemos llamar a nuestra función asíncrona así:

AsyncFnc.begin(p1, p2, (obj,res) => {

    int retval;
    retval=AsyncFnc.end(res, out p3, out p4);

    // resto del código del callback
    [...]
});

De esta manera, cuando la función asíncrona llegue al final y retorne, se ejecutará el código de callback contenido en el closure. Vemos también que llamamos a AsyncFnc.end; esta es, precisamente, la manera que tenemos de obtener el valor devuelto por la función asíncrona, así como los parámetros de salida. También es esta llamada la que debemos rodear con un try/catch() si queremos capturar cualquier excepción lanzada desde dentro de la función asíncrona.

Así pues, ya sabemos llamar a una función asíncrona, y sabemos como recibir sus resultados. ¿Pero como hacemos para que ceda el control y devolvérselo? No olvidemos que NO se tratan de varios threads paralelos, sino de un único hilo de ejecución.

La respuesta está en yield y .callback(). La primera es una llamada que, al realizarla dentro de una función asíncrona, devuelve el control a la función llamante. La segunda es un método de la función asíncrona (igual que .begin y .end; podemos comparar una función asíncrona con un objeto que ofrece esos tres métodos públicos) que lo que hace es continuar la ejecución de dicha función justo después del último yield ejecutado. Un detalle interesante es que las llamadas a yield se pueden situar absolutamente en cualquier parte de una función asíncrona, incluso dentro de un bucle, lo que nos permite, de una manera sencilla, devolver el control en cada iteración para que se vayan ejecutando otras partes del código, y recuperarlo para la siguiente iteración.

Ahora que ya tenemos la imagen completa podemos ver como se ejecuta el código: cuando una función normal llama a una función asíncrona, le cede el control y ésta comienza a ejecutarse hasta alcanzar el primer yield. En ese punto la llamada retorna a la función normal, y sigue ejecutándose el código situado a continuación de la llamada. Cuando, en algún punto posterior, se llame al .callback() de la función asíncrona, el control pasará al código situado justo después de dicho primer yield, y seguirá ejecutándose hasta que encuentre otro yield, momento en que el control de la ejecución pasará al código situado justo después de la llamada a .callback(). Este juego de ping-pong continuará hasta que la función asíncrona llegue al final y retorne. En ese momento se ejecutará el código del callback y continuará la ejecución después de la última llamada a .callback().

Un ejemplo gráfico

Para entender mejor este proceso, veámoslo en forma gráfica. Por cuestiones de claridad lo dividiremos en dos imágenes: en la primera veremos el flujo desde que se inicia la función asíncrona hasta que está a punto de acabar, y en la segunda desde que se ejecuta el return al final.

En la imagen anterior vemos la primera parte del ciclo de vida de una función asíncrona. Cada función está representada por un rectángulo, y dentro de ella, el flujo de ejecución natural es de arriba a abajo.

Desde el Main Loop de la aplicación se llama a un callback (por ejemplo, porque el usuario ha pulsado un botón en la interfaz gráfica), llamado Function en este ejemplo, y en éste se llama a nuestra función asíncrona (llamada AsyncFnc en el ejemplo), que comienza a ejecutarse. En el momento en el que se alcanza el primer yield, el control vuelve al callback que la lanzó, que sigue su ejecución hasta que, en un punto, éste llama a AsyncFnc.callback(). En ese momento el flujo de ejecución vuelve a la función asíncrona, que continúa justo en la instrucción que sigue al primer yield. Un poco después se llega a un segundo yield, que devuelve el control al callback original.

Cuando el callback termina, se devuelve el control al Main Loop, como siempre. Pero la función asíncrona aún no ha terminado. Vemos entonces que en dos puntos determinados se llama a AsyncFnc.callback(). Estas llamadas no son automáticas, sino que tuvieron que ser preprogramadas (por ejemplo con un temporizador, o con una llamada a Idle.add(AsyncFnc.callback) para que se ejecuten en un momento en que no hay eventos por despachar). Cada vez que se llama a dicha función, la ejecución de la función asíncrona continúa justo después del último yield, hasta que encuentra otro, o bien hasta que finaliza la función.

En el siguiente esquema, las flechas en rojo muestran el flujo de ejecución cuando la función asíncrona llega al final: en ese momento se ejecuta el callback registrado cuando se lanzó la función (si es que existe, claro; si no se definió, se retorna directamente), y cuando se termina éste, se retorna al punto de llamada.

Llamar a una función asíncrona desde otra función asíncrona

A la hora de llamar a una función asíncrona desde otra función asíncrona, la primera idea que nos puede venir a la cabeza es hacer

// ATENCION, ESTE CODIGO NO FUNCIONARA CORRECTAMENTE

async void AsyncFnc1() {
    int retval;

    [...]
    AsyncFnc2( (ob,res) => {
        retval=AsyncFnc2.end();
        AsyncFnc1.callback();
    });
    yield;
    [...]
}

Aparentemente, este código lanzaría AsyncFnc2 y suspendería la ejecución de AsyncFnc1, pero de manera que, al terminarse la ejecución de AsyncFnc2, continuaría la ejecución de AsyncFnc1.

Por desgracia, ese código es probable que no funcione correctamente, como mínimo en un caso concreto: si, por la razón que sea, la función AsyncFnc2 finaliza sin llamar a yield al menos una vez, se llamará antes a AsyncFnc1.callback() que al yield, por lo que el código fallará. Una solución a este problema sería utilizar Idle.add(AsyncFnc1.callback) en su lugar, pero sólo funcionará si tenemos un Main Loop en nuestro programa, pues será éste quien llame al callback después de que la función asíncrona retorne tras ejecutar el yield.

Afortunadamente, los diseñadores de Vala eran conscientes de que es muy común llamar a una función asíncrona desde otra, por lo que simplificaron notablemente este caso concreto. Así, para llamar a AsyncFnc2 desde AsyncFnc1, de manera que ésta última continúe su ejecución cuando acabe la otra, sólo hay que hacer

async void AsyncFnc1() {
    int retval;

    [...]
    retval=yield AsyncFnc2();
    [...]
}

Otra ventaja es que, en este caso, no necesitamos llamar a AsyncFnc2.end() para obtener los resultados, porque se hace directamente.

El flujo de ejecución sería así:

Vemos que desde el Main Loop llamamos a nuestra primera función asíncrona, AsyncFnc1, y después de un trozo de código llamamos, mediante yield, a nuestra segunda función asíncrona, AsyncFnc2. En ésta ejecutamos algo de código y llegamos a un yield. Sin embargo, éste no retorna a AsyncFnc1, sino a la llamada anterior. Esto es así porque AsyncFnc1 lo que quiere es dormir hasta que AsyncFnc2 termine.

También vemos que para reanudar la ejecución es preciso llamar a AsyncFnc2.callback(). Esto debe tenerse muy en cuenta: en este instante es incorrecto llamar a AsyncFnc1.callback(). Por eso es fundamental que cada función asíncrona se encargue de programar su llamada para recuperar el control, y no delegarlo a funciones externas, pues es muy fácil cometer errores.

Cuando AsyncFnc2 llega al final, se cede el control al código de AsyncFnc1, que sigue ejecutándose, y al terminar, se llamaría a la función de callback de finalización (si la hubiera), y seguiría la ejecución justo a continuación de la llamada a AsyncFnc2.callback() (que, en este caso, simplemente seguiría con la ejecución del Main Loop).

Casos de uso de funciones asíncronas

El primer caso de uso de las funciones asíncronas es para llamar a otras funciones asíncronas. Esto, que puede parecer una perogrullada, toma sentido en cuanto descubrimos las funciones asíncronas de Gio. Un ejemplo es el de File.copy_async: este método de la clase File copia un fichero de manera asíncrona. De no existir, tendríamos que crear un hilo y ejecutar en él la función síncrona de copia, porque de no hacerlo así, si el fichero fuese muy grande la interfaz gráfica de nuestro programa se quedaría congelada durante la operación.

Hay dos maneras de llamar a estas funciones asíncronas: la primera es la clásica, incluyendo un callback (bien como puntero a función, bien como función anónima o closure) que se llame cuando termine la operación; la segunda, más cómoda en según que casos, es llamarla mediante yield desde una función asíncrona de Vala. En este segundo caso hay que saltarse el parámetro del callback.

El segundo caso de uso es para llamar a servicios externos mediante DBus. De esta manera podemos aprovechar el tiempo entre el envío de la petición y la llegada de la respuesta. Para ello, basta con añadir async en la definición de la interfaz DBus. Un ejemplo de como definir la interfaz DBus para formatear un disco, mediante el demonio UDisks:

[DBus (name = "org.freedesktop.UDisks.Device")]
interface Device_if : GLib.Object {
	public abstract async void FilesystemCreate(string type, string[] options) throws IOError;
}

Por desgracia, en este caso concreto no podemos aprovechar la funcionalidad de la llamada mediante yield, debido a que existe un timeout en las llamadas a métodos DBus de, aproximadamente, 25 segundos, y se necesita más tiempo para formatear algunos soportes. Se supone que se puede ajustar este timeout mediante un modificador de Vala, pero después de consultar en la lista de correo y de buscar en internet, no he conseguido que funcione. Si alguna alma caritativa nos puede iluminar…

El siguiente caso de uso es cuando se quiere realizar una tarea larga y que consuma CPU, pero no se quieren usar threads o lanzar un proceso paralelo, sino ejecutarlo en los momentos en los que el proceso actual esté inactivo (en el caso de una aplicación gráfica, cuando no hay eventos que procesar en la cola). La solución consiste en crear una función asíncrona que realice la tarea, insertando llamadas a yield de manera regular. Para que la función se despierte cuando la cola está vacía, usamos Idle.add() para llamar al método .callback() de nuestra función asíncrona, así:

public async void AsyncFnc() {

    // Inicialización de variables y demás
    [...]

    // Comienza la tarea
    while(cond==true) {

        // Realizamos una iteración de la tarea
        [....]

        // Avisamos al Main Loop que queremos que nos despierte
        // cuando no haya eventos pendientes
        Idle.add(AsyncFnc.callback);

        // Y devolvemos el control al búcle principal o Main Loop
        yield;
    }

    // La tarea ha finalizado
    // Limpiamos todo
    [...]
}

Por supuesto, podemos utilizar también un temporizador en lugar de Idle, o cualquier otro sistema que decidamos.

Un detalle interesante que se puede ver en este ejemplo es que la llamada a yield puede ir dentro de un bucle while o for sin ningún problema.

Por último, también son útiles para sincronizar threads. El truco en este caso está en almacenar un puntero al método de callback de la función asíncrona que queremos sincronizar, lanzar el thread, y al final de éste, añadir dicho callback a la cola de mensajes. El esquema es éste:

async void AsyncFunc() {

    // Aquí almacenamos el método .callback() de nuestra función asíncrona
    SourceFunc callback = AsyncFunc.callback;

    // Creamos la función para el thread. La hacemos anónima (closure) para que tenga acceso a la
    // variable local callback.
    ThreadFunc<void*> run = () => {
        // Código del thread. Aquí hacemos las operaciones deseadas
        [...]

        // Aquí ya hemos terminado las operaciones, así que
        // añadimos el método .callback() a la cola para que se llame inmediatamente
        Idle.add((owned) callback);
        return null;
    };

    // Lanzamos el thread
    Thread.create<void*>(run, false);

    // Y esperamos a que se llame a nuestra función callback
    // que será cuando termine el thread
    yield;
}

En la documentación oficial se encuentran tres ejemplos de funciones asíncronas, muy útiles para entender aún mejor esta útil característica de Vala. En concreto aparece un ejemplo del método de sincronización de threads, donde se puede ver mucho mejor la sintaxis concreta.

DBus en Vala

martes, agosto 23rd, 2011

Actualizado Estoy trabajando en un nuevo proyecto, escrito íntegramente en Vala, y la verdad es que cuantas más cosas aprendo sobre él, más me gusta.

Lo último que descubrí fue como trabajar con DBus en Vala, y resulta que es extremadamente sencillo y elegante, aunque tuve que investigar bastante hasta encontrar como hacer algunas cosas, pues los ejemplos que vienen en la página tratan de como trabajar con un servidor hecho por uno mismo, cuando yo necesitaba acceder a un servicio del sistema.

Así pues, y sin más dilación, veamos como se puede usar DBus para obtener una lista de los discos duros conectados por USB al ordenador, y como formatearlos.

Lo primero que vemos es que toda esa información la gestiona el demonio UDisk, el cual exporta una serie de funciones y objetos a través de DBus para que cualquier programa pueda acceder a ellos. Si echamos un vistazo a la documentación de la API, vemos que se exporta el método EnumerateDevices, el cual nos devuelve un array de objetos.

¿Y como accedemos a este método del objeto UDisk desde Vala? Pues con este código:

[DBus (name = "org.freedesktop.UDisks")]
interface UDisk_if : GLib.Object {
    public abstract ObjectPath[] EnumerateDevices() throws IOError;
}

UDisk_if udisk = Bus.get_proxy_sync<UDisk_if> (BusType.SYSTEM, "org.freedesktop.UDisks","/org/freedesktop/UDisks");
var retval = udisk.EnumerateDevices();

Aquí vemos que primero definimos una interfaz con todos los métodos a los que vamos a querer acceder (en este caso sólo uno), sacando de la documentación los parámetros y demás, y le añadimos antes una cabecera con el nombre del objeto. Luego, creamos un proxy síncrono, conectándonos a través del bus del sistema (BusType.SYSTEM), al servidor UDisk (org.freedesktop.UDisks) y pedimos acceso al objeto deseado (/org/freedesktop/UDisks). Finalmente, llamamos al método deseado, almacenando el resultado en una nueva variable.

Es importante recalcar que cualquier método que se defina puede emitir, siempre, una excepción de tipo IOError, por lo que siempre se debe añadir al final de la definición la coletilla throws IOError. De no hacerlo se producirá un error de compilación. También comentar que dado que esta función sólo devuelve un parámetro, la he escrito en formato resultado metodo(parametros); sin embargo, también es perfectamente válida la sintaxis void metodo (parametros,…, out resultado). Esta segunda forma es obligatoria cuando un método devuelve más de un resultado. Con esa sintaxis, la interfaz tendría esta forma:

[DBus (name = "org.freedesktop.UDisks")]
interface UDisk_if : GLib.Object {
    public abstract void EnumerateDevices(out ObjectPath[] path) throws IOError;
}

En retval tenemos ahora un array de objetos ObjectPath, cada uno de tipo Device, representando una unidad extraíble. ¿Y ahora, qué? Pues ahora vamos a imprimir el punto de montaje de cada uno de ellos. Si miramos la documentación del objeto Device, vemos que dicha información se almacena como una propiedad, así que definimos nuestra interfaz así:

[DBus (name = "org.freedesktop.UDisks.Device")]
interface Device_if : GLib.Object {
    public abstract string IdLabel { owned get; }
    public abstract string[] DeviceMountPaths { owned get; }

    public abstract void FilesystemUnmount(string[] options) throws IOError;
    public abstract void FilesystemCreate(string type, string[] options) throws IOError;
    public abstract void PartitionModify (string type, string label, string[] options) throws IOError;
    public abstract void FilesystemMount(string type, string[] options, out string mount_path) throws IOError;
}

Ponemos { owned get; } en lugar de {owned get; set; } porque los campos son de sólo lectura. También he añadido los cuatro métodos necesarios para poder formatear una partición, aunque en el ejemplo no los voy a utilizar.

Ahora llega el momento del código que accede a los datos, que es éste:

Device_if device2;
foreach (ObjectPath o in retval) {
    device2 = Bus.get_proxy_sync<Device_if> (BusType.SYSTEM, "org.freedesktop.UDisks",o);
    GLib.stdout.printf("Disco %s montado en:n",device2.IdLabel);
    foreach (string s in device2.DeviceMountPaths) {
        GLib.stdout.printf("    %sn",s);
    }
}

Así, primero recorremos la lista de objetos que obtuvimos con la anterior llamada (en retval), y creamos un proxy síncrono para cada uno de ellos, contra el servidor de UDisk (él es quien nos dio la lista de objetos, por lo que estos tienen que estar en él). Luego imprimimos la etiqueta simplemente leyendo la propiedad del objeto, y por último imprimimos todos los puntos de montaje (una misma partición puede estar montada en varios sitios a la vez).

Actualización: Uno de los problemas que tuve fue que la llamada a FilesystemCreate (que formatea la unidad especificada) tarda varios segundos en ejecutarse, lo que hacía que la GUI se quedase colgada. La primera solución que encontré fue ejecutarla en un thread aparte, y usar espera activa para detectar cuando terminó, pero era muy poco elegante. La solución definitiva consiste en realizar llamadas asíncronas a DBus, explicadas en el tutorial de Vala. La idea consiste en añadir la palabra reservada async en la definición de la función en la clase, con lo que la llamada a DBus será no-bloqueante, y definir un callback anónimo.

[DBus (name = "org.freedesktop.UDisks.Device")]
interface Device_if : GLib.Object {
  [...]
  public abstract async void FilesystemCreate(string type, string[] options) throws IOError;
  [...]
}

  // Llamamos a la función, añadiéndo como último parámetro
  // una función anónima que se ejecutará al terminar la
  // ejecución
  device2.FilesystemCreate.begin(format,options, (obj,res) => {
    try {
      // La llamada a .end recoge los valores devueltos (de haberlos)
      // y dispara cualquier excepción que se haya producido
      device2.FilesystemCreate.end(res);
      return;
    } catch (IOError e) {
      // Captura de excepciones (en este caso, fallo durante el formateo)
    }
  });

Una vez que hemos hecho la llamada, podemos volver al bucle principal de GTK o llamar a la función run() de un cuadro de diálogo, y éste seguirá funcionando en paralelo a la ejecución de nuestra llamada a DBus.

Enlace dinamico

sábado, febrero 19th, 2011

Acabo de lanzar la versión 8.0 de FPT_BT4LXMedia. La primera novedad es la nueva versión de Transmission, la 2.21. La segunda es que, por fin, los ejecutables utilizan enlazado dinámico.

Como recordareis, las versiones anteriores utilizaban enlazado estático porque no conseguía que arrancasen desde el disco, a pesar de utilizar LD_LIBRARY_PATH para apuntar al directorio con las nuevas bibliotecas. Para los que no sepan qué es eso, es una variable de entorno que permite añadir nuevas rutas en donde buscar bibliotecas de enlace dinámico, y éstas tienen prioridad sobre las “normales” del sistema. Así, si tengo un programa enlazado con una versión concreta de libc, diferente de la del sistema, puedo instalarla en otro directorio (por ejemplo, /tmp/libs) y lanzar el programa con:

LD_LIBRARY_PATH=/tmp/libs mi_programa

Por desgracia, esto no funcionaba en mi disco duro. Tras investigar mucho, descubrí que el problema estaba en el enlazador dinámico en tiempo de arranque. Resulta que el núcleo sólo es capaz de arrancar, por sí solo, binarios que estén enlazados estáticamente. Para los dinámicos lo que hace es delegar en /lib/ld-linux.so (o /lib/ld-uclibc en la biblioteca uClib). A pesar de llevar extensión .so, este fichero es realmente un programa (enlazado estáticamente, claro) diseñado para cargar en memoria el programa dinámico deseado, analizar qué bibliotecas necesita, cargarlas en memoria si aún no están, realizar toda la operación de enlazado y, finalmente, lanzar el código.

Por desgracia, la versión del enlazador dinámico de la gentoo que utilizo para desarrollar es más reciente que la de mi disco duro, lo que hacía que recibiese un error cada vez que intentaba ejecutar un programa compilado directamente. Para complicar aún más las cosas, la ruta del enlazador dinámico está especificada en cada ejecutable de manera inamovible, estableciéndose dicho valor durante la compilación. Y por si fuera poco, el enlazador de Busybox no permite su ejecución directa, por lo que tampoco podía hacer un simple /lib/ld-uclibc mi_programa.

Ante todo esto, la única solución que me quedó fue la de copiar el enlazador dinámico del entorno de desarrollo en una carpeta diferente y, al compilar bftpd, transmission y ctransmission, especificar la ruta deseada. Esto se hace en la etapa final de enlazado mediante la opción -Wl,–dynamic-linker=path/al/enlazador. De esta manera bastó con copiar el enlazador y el resto de bibliotecas a /tmp/hdd/root/libs y lanzar los programas con

LD_LIBRARY_PATH=/tmp/hdd/root/libs mi_programa

Con esto espero ahorrar algo de memoria. También haré algunas pruebas para ver si los programas funcionan bien con la versión de la uClib del propio disco duro, en cuyo caso podría ahorrar aún más.

Entre signos anda el juego

miércoles, enero 12th, 2011

Una de las frases preferidas de mi madre es Está el viejo muriendo, y está aprendiendo; y como en la gran mayoría de los refranes, hay una grandísima verdad oculta en ella.

Y es que estos días estoy peleándome con el micro ARM de un TomTom, y me encontraba con que el código que iba perfectamente en mi PC daba un fallo rarísimo en el GPS: al imprimir el carácter ‘/’ no lo hacía bien; pero sólo ese, el resto los renderizaba perfectamente. Después de mucho depurar, descubrí que el fallo era debido, nada menos, que al signo de una variable. Y es que, a pesar de utilizar el mismo compilador (GCC) para ambos casos, resulta que un char a secas en PC es signed, como todo el mundo asume, pero en ARM es unsigned, por lo que un valor que tenía que ser -1 se convertía en 255 (la segunda parte del problema es que luego operaba con ese valor y con otro de 32 bits, con lo que, al añadirle los 24 bits restantes, no expandía el bit de signo). Una vez añadido el modificador correspondiente, todo pasó a funcionar perfectamente.

Así pues, nunca deis por supuesto el signo de una variable. Consejo de amigo.

Desplazando bits

viernes, diciembre 31st, 2010

Hace mucho tiempo, cuando empecé a programar en C, leí una curiosa advertencia sobre los operadores de desplazamiento de bits, los famosos >> y <<. Se afirmaba allí que, al desplazar una variable con ellos, los bits que salían se perdían (lógico, siendo un desplazamiento), pero que los bits que entraban por el lado opuesto tenían un valor indefinido, que podía ser cero o uno según la implementación.

Es cierto que todos los programadores asumen que lo que entra es cero siempre; pero pese a todo, yo, que tiendo a ser demasiado cauto, opté por añadir siempre una máscara AND para poner a cero los bits entrantes y así evitarme sorpresas.

Sin embargo, hace unos días, mientras optimizaba unas rutinas que hacen uso de desplazamientos, decidí investigar más a fondo el tema, porque a fin de cuentas una máscara consume ciclos de reloj, así que si había alguna forma de evitarla siempre sería una mejora. Y lo que descubrí me dejó gratamente sorprendido, porque en realidad el valor de los bits que entran no depende del compilador, sino que está perfectamente definido en el estándar.

Cuando se utiliza el desplazamiento hacia la izquierda (<<), por el lado derecho siempre entran ceros. Ahí nunca hay problemas. Por otro lado, cuando se utiliza el desplazamiento hacia la derecha (>>) en una variable sin signo (por ejemplo, en un unsigned int), por la izquierda también entran siempre ceros.

Sin embargo, cuando utilizamos variables con signo (por ejemplo, short int) en un desplazamiento hacia la derecha, los bits que entren por la izquierda serán una copia del bit de mayor peso del valor original. Así, si teníamos un número negativo (su bit de mayor peso está a uno), al desplazarlo hacia la derecha seguirá siendo negativo, porque los bits entrantes serán unos; en cambio, si era un número positivo (su bit de mayor peso está a cero) los bits entrantes serán cero.

Supongo que la razón de hacerlo así es que, si se utilizan rotaciones para hacer divisiones, se conserva el signo.

Este código ilustra perfectamente lo dicho aquí:

#include <stdio.h>

void desplaza(unsigned int o) {

  signed int s1,s2;
  unsigned int u1,u2;

  s1=s2=u1=u2=o; // asignamos el mismo valor a las cuatro variables

  s1>>=4; // desplazamos cuatro veces cada una de ellas
  s2<<=4;
  u1>>=4;
  u2<<=4;

  printf("Con signo:ntdesplazamiento a la derecha   %08Xn",s1);
  printf("tdesplazamiento a la izquierda %08Xn",s2);
  printf("Sin signo:ntdesplazamiento a la derecha   %08Xn",u1);
  printf("tdesplazamiento a la izquierda %08Xnn",u2);
}

int main() {

  printf("nDesplazamiento de 4 posiciones.n");
  printf("Valor original: 0xA50000A5 (bit de mayor peso a uno).n");
  desplaza(0xA50000A5);
  printf("nValor original: 0x5A00005A (bit de mayor peso a cero).n");
  desplaza(0x5A00005A);

  return 0;
}

La salida es la siguiente, donde se aprecia claramente como el bit superior se replica en los desplazamientos a la derecha de las variables con signo, pero no en el resto de los casos:

Desplazamiento de 4 posiciones.
Valor original: 0xA50000A5 (bit de mayor peso a uno).
Con signo:
  desplazamiento a la derecha   FA50000A
  desplazamiento a la izquierda 50000A50
Sin signo:
  desplazamiento a la derecha   0A50000A
  desplazamiento a la izquierda 50000A50

Valor original: 0x5A00005A (bit de mayor peso a cero).
Con signo:
  desplazamiento a la derecha   05A00005
  desplazamiento a la izquierda A00005A0
Sin signo:
  desplazamiento a la derecha   05A00005
  desplazamiento a la izquierda A00005A0

Trabajando con señales

domingo, abril 11th, 2010

Estos días he seguido trasteando con Bftpd, y me puse a trabajar en resolver un problema bastante molesto: la cantidad de procesos zombie que deja tras de sí.

Para entender lo que pasa, primero hay que explicar que Bftpd lanza un nuevo proceso por cada nuevo usuario que se conecta, en lugar de utilizar un solo proceso para gestionar todas las conexiones. Así, tenemos un proceso padre que se limita a escuchar, y cada vez que llega una petición de conexión crea un proceso hijo para que la gestione; también comprueba cuando un hijo se muere (porque se haya cerrado la conexión).

Y aquí es de donde surge el problema: cada vez que un hijo termina, emite una señal (en concreto SIGCHLD) hacia el padre, el cual, al recibirla, ejecutará una pequeña función o callback y luego seguirá su ejecución normal. En el caso de Bftpd, dicho callback comprueba qué hijo es el que se ha muerto, pide su valor de retorno (para evitar que se quede zombie) y libera una serie de recursos que le reservó. En concreto, el código de dicha función comienza con un:

pid = wait(NULL);
[codigo para liberar los recursos del proceso PID]

Hasta aquí todo parece correcto; por desgracia, después de cada sesión con Bftpd quedaban varios procesos zombie. La razón era, obviamente, que el proceso padre no había leído su valor de retorno, por lo que el sistema operativo no los podía hacer desaparecer; sin embargo, en el callback se lee siempre dicho valor, así que es obvio que algo raro estaba pasando.

Para depurar el código empecé por añadir unos printfs: uno en el punto en que se crean los procesos hijo, mostrando su PID; otro en el punto de finalización de dichos hijos, y otro en la función de callback de SIGCHLD. Ejecuté el servidor, hice unas cuantas operaciones, vi la salida por pantalla, y… ¡Sorpresa! ¡Había menos llamadas a la función de callback que muertes! Si todo funcionase como se esperaba, el número debería ser exactamente el mismo (cada muerte debería emitir una señal SIGCHLD, la cual haría que se ejecutase el callback). Sin embargo, por alguna misteriosa razón, algunos procesos morían sin emitir la señal.

Para aquellos a los que les de igual el por qué, y simplemente necesiten saber el como, daré primero la solución: se trata de leer los PIDs de todos los hijos muertos en cada llamada al callback, en lugar de leer el de uno sólo. En otras palabras, el código del callback para SIGCHLD debe ser:

do {
        pid = waitpid(-1,NULL,WNOHANG);
        if (pid>0) {
            [codigo para liberar los recursos del proceso PID]
        }
} while (pid>0);

Vale, pero ¿por qué ocurre esto?

Para entender lo que ocurre hay que irse un poco a las profundidades del núcleo, porque se debe a un problema de cómo están implementadas las señales.

En Linux, cada proceso tiene un conjunto de bits, y cada uno representa una señal. Cuando un proceso quiere enviar una señal a otro, lo que hace realmente es poner a uno el bit de dicha señal en el proceso receptor.

¿Y cuando comprueba el receptor la llegada de una señal? En cada cambio de contexto: en un sistema operativo multitarea, los procesos se van turnando en el uso de la CPU, de manera que primero la usa unos milisegundos el proceso A, luego otros milisegundos el proceso B, y así sucesivamente hasta que se acaba la lista y se vuelve al proceso A. Cada vez que se cambia de un proceso al siguiente se realiza un cambio de contexto, en el que primero se guarda el estado del proceso actual, se busca quien será el siguiente proceso, se carga su estado y se le cede el control. Pero, y aquí está la cuestión, antes de este último paso se comprueba la máscara de bits de las señales, y si alguna está activa se ejecutará primero el callback correspondiente.

¿Y por qué se pierden entonces señales SIGCHLD? Pues porque dos o más procesos se mueren “a la vez”; esto es, se mueren dentro del mismo intervalo entre dos ejecuciones del proceso padre. Así, lo que ocurre es que cuando se muere el primero, pone a 1 el bit de la señal SIGCHLD del proceso padre; el repartidor de tareas, al ver que ha muerto, libera todo lo que puede y pasa al siguiente proceso; éste muere también, y también pone a 1 el bit de la señal SIGCHLD del proceso padre… pero ese bit ya estaba a uno, por lo que se queda como está. Finalmente, el repartidor decidirá que es el momento de ejecutar de nuevo el proceso padre, pero antes verá que el bit de la señal SIGCHLD está activo, por lo que lo pondrá a cero y  llamará al callback. El resultado: se murieron dos procesos pero sólo se ejecutó una vez la función asociada a la señal, por culpa de que las señales se almacenan con un único bit.

Es por esto que la solución indicada arriba funciona: la señal nos indica que se ha muerto AL MENOS un proceso hijo, así que debemos comprobar todos, y no asumir que cada uno enviará una señal.

Este caso nos demuestra que las señales son un sistema de comunicación bastante frágil, por lo que no se debe abusar de él. Cosas como “contar el número de veces que llega una señal” o similares pueden dar problemas incluso si hay un único emisor, porque si emite varias veces la misma señal antes de que el padre recupere el control de la CPU, contarán como una única señal. Y como muestra para los incrédulos, un pequeño programita que ejemplifica todo esto:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>

int v;

void senal(int valor) {

 printf("Recibidan");
 v++;
}

int main(int argc, char **argv) {

 int pid1,pid2,pid3,pid4,loop;

 v=0;
 signal(SIGUSR1,senal);
 pid1=getpid();
 pid2=fork();
 if(pid2==0) {
 fork();
 fork();
 printf("hijon");
 kill(pid1,SIGUSR1);
 kill(pid1,SIGUSR1);
 kill(pid1,SIGUSR1);
 kill(pid1,SIGUSR1);
 kill(pid1,SIGUSR1);
 sleep(2);
 } else {
 printf("padren");
 for(loop=0;loop<21;loop++) {
 sleep(1);
 }
 printf("Total: %dn",v);
 }
}

Este código crea un primer hijo, el cual se divide en cuatro hijos en total, y cada uno emite, de golpe, cinco señales SIGUSR1 al padre. Sin embargo, al ejecutarlo veremos que nunca se detectan todas ellas, sino, como mucho, una por proceso hijo (y a veces ni eso).

Mono

lunes, agosto 31st, 2009

A principios de verano surgió una agria polémica sobre la conveniencia o no de usar Mono (la implementación libre de los estándares ECMA-334 y ECMA-335) dentro de GNOME. El problema en buena medida venía dado por las diferentes opiniones sobre la libertad de Mono. Los que estaban a favor afirmaban que Microsoft había reconocido de manera explícita que renunciaba a denunciar a nadie por usar sus patentes sobre ambos estándares, mientras que los que estaban en contra afirmaban que dicho texto legal era lo suficientemente obtuso como para que no garantizase prácticamente nada, y que habría que abandonar por completo Mono, proponiendo algunos reemplazarlo por Vala. En su momento no escribí nada, en parte porque ya había suficiente polémica, y en parte por falta de tiempo, así que ahora que los ánimos están más calmados me gustaría dar mi opinión sobre una cuestión relacionada.

Para empezar, todo el mundo se centra directamente en si hay riesgo de que haya demandas por patentes a la hora de usar Mono. En ese punto no voy a entrar puesto que se trata de una cuestión legal y yo no soy abogado. Donde sí me gustaría entrar es en el tema de qué aplicaciones es razonable que se escriban en Mono (y, en realidad, en cualquier otro lenguaje que trabaje sobre un runtime, como Java o Python) y cuales deberían ir siempre en Vala.

Para presentar el problema me iré al caso de un viejo conocido de todo el mundo: Windows Vista. Tras su lanzamiento surgieron grandes críticas por su excesivo uso de memoria, lo que obligaba a instalar al menos 2GB para conseguir una fluidez equivalente a la de un Windows XP con 512MB.

Y ese es el problema de utilizar Mono, Java, Perl o Python en donde no se deben usar. Recordemos que el usuario utiliza un ordenador con un fin concreto: realizar un trabajo. Para ello utiliza un programa determinado: si quiere dibujar, utilizará un programa de dibujo; si quiere escribir una carta, utilizará un procesador de texto, etc. Una vez que ha terminado la tarea, cierra el programa (liberando los recursos utilizados) y abre otro para hacer otra tarea.

Por otro lado, tanto los usuarios como los programas necesitan de un sistema operativo que actúe como intermediario. Es importante recalcar que el sistema operativo, hasta cierto punto, debería ser transparente: el usuario no utiliza el sistema operativo para hacer sus trabajos, sino los programas concretos. Así pues, el sistema operativo debe ser parco en el consumo de recursos, pues éstos deben estar disponibles para los programas.

El problema surge cuando hacemos parte del sistema operativo en un lenguaje interpretado o que necesite una máquina virtual. En ese momento nos encontraremos con un consumo mayor de memoria y de procesador, lo que significa que está consumiendo recursos que no debería. Un ejemplo de esto sería programar applets de escritorio o extensiones del gestor de archivos utilizando Mono o Python, o demonios que se queden en ejecución en segundo plano durante toda la sesión. Sin embargo, una aplicación sí puede estar escrita en estos lenguajes porque, a fin de cuentas, es lo que el usuario quiere utilizar. Podría ser deseable que utilice menos recursos, pero ya no es algo tan crítico porque aquí sí estamos utilizando lo que, a fin de cuentas, nos corresponde. Y por aplicación incluyo aquí también aplicaciones oficiales de Gnome, como por ejemplo, Banshee o F-Spot. La clave está en si son programas que permanecen abiertos durante toda la sesión y consumiendo recursos, o son aplicaciones que el usuario lanza expresamente cuando quiere realizar una tarea y que cierra cuando ha terminado de hacerla, liberando los recursos que utilizó. No olvidemos que una aplicación se puede cerrar y volver a abrir, mientras que un applet o un demonio tienen que estar corriendo constantemente para cumplir su función.

¿Entonces debemos utilizar siempre C para realizar las aplicaciones de escritorio, complementos y demonios? Aparentemente sería lo deseable; sin embargo, tiene el grave problema de que programar en C no es tan cómodo, ni los programas son tan mantenibles, como programar con Python, C# o Java (por algo existen). Y es justo aquí donde Vala brilla con luz propia: al tratarse de un lenguaje de alto nivel, con gestión de memoria asistida y una biblioteca de funciones y componentes que crece día a día, simplifica la escritura y mantenimiento del código hasta el nivel de los otros lenguajes comentados; y al compilarse directamente a código nativo, sin necesidad de ningún runtime extra, el resultado es prácticamente tan bueno como si se hubiese utilizado C desde el principio. Por supuesto, Vala también se puede utilizar para escribir aplicaciones, pero considero que aquí ya es cuestión de preferencias, así que cada uno utilice lo que más le guste: Perl, Python, C, Vala, Mono, Lisp, Ook… Pero para partes del sistema operativo o del escritorio considero que sólo se debería utilizar Vala.

Y debo reconocer que consejos vendo pero para mí no tengo, porque una de mis aplicaciones (GtkPSproc) está escrita justo al revés: el programa principal, que sólo se lanza cuando el usuario quiere imprimir algo, y que lleva la interfaz gráfica y hace todo el trabajo, está escrito en C, mientras que un pequeño applet (que, por tanto, está ejecutándose desde que se inicia el escritorio hasta que se apaga la máquina) que se encarga de reenviar la impresión al programa principal, está escrito en Python. En mi defensa diré que la aplicación principal la escribí antes de aprender Python, y que escribir el applet en python fue una chapuza inicial que acabó quedando como definitiva (¿Cuela?).

Vala de plata

domingo, agosto 2nd, 2009

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.


Utilizamos cookies para garantizar que tenga la mejor experiencia en nuestro sitio web.