Glib

Tabla de Contenidos

  1. Tipos de datos de GLib
  2. Mensajes de salida
    1. Mensajes de salida.
    2. Funciones de depuración.
    3. Funciones de registro.
  3. Trabajar con cadenas
    1. Manipular el contenido de una cadena.
      1. Duplicación de cadenas.
      2. Verificación de caracteres.
      3. Copia y concatenación de cadenas.
    2. GString : la otra manera de ver una cadena.
      1. Cómo se aloja una cadena con GString.
      2. Cómo puedo liberar un GString
      3. Añadir, insertar y borrar cadenas en un GString.
      4. Introducir texto preformateado a un GString.
      5. Otras funciones útiles para el manejo de GString.
    3. GQuarks.
  4. Jugando con el tiempo
    1. Funciones de manejo de fechas y horas.
      1. Creación de una nueva fecha.
    2. Midiendo intervalos de tiempo con GTimer.
      1. Descripción de las funciones para manejar GTimer.
  5. Miscelánea de funciones
    1. Números aleatorios.
    2. Funciones de información sobre entorno.
  6. Bucles de ejecución
    1. Alarmas
    2. Tiempos de inactividad.
  7. Tratamiento de ficheros y canales de Entrada/Salida
    1. Obtención de un GIOChannel.
    2. Generalidades en el trabajo con GIOChannels.
    3. Operaciones básicas.
    4. Integración de canales al bucle de eventos.
    5. Configuración avanzada de canales.
      1. Codificación
      2. Terminador de línea.
      3. Configuración de buffer interno.
  8. Manejo de memoria dinámica
    1. Reserva de memoria.
    2. Liberación de memoria.
    3. Realojamiento de memoria.
    4. Perfil de uso de memoria.
  9. Estructuras de datos: listas enlazadas, pilas y colas
    1. Listas enlazadas.
      1. Introducción
      2. GSList
    2. Listas doblemente enlazadas.
      1. Introducción.
      2. GList
    3. GQueue: pilas y colas.
      1. Colas
        1. Iniciar cola.
        2. Cola vacía.
        3. Consultar el frente.
        4. Consultar el final.
        5. Meter
        6. Sacar
        7. Vaciar cola.
      2. Pilas
        1. Iniciar pila.
        2. Pila vacía.
        3. Consultar pila.
        4. Meter
        5. Sacar
        6. Vaciar pila.
  10. Estructuras de datos avanzadas
    1. Tablas de dispersión.
      1. Qué es una tabla de dispersión.
      2. Cómo se crea una tabla de dispersión.
      3. Cómo manipular un tabla de dispersión.
      4. Búsqueda de información dentro de la tabla.
      5. Manipulación avanzada de tablas de dispersión.
    2. Arboles binarios balanceados
      1. Creación de un árbol binario.
      2. Agregar y eliminar elementos a un árbol binario.
      3. Búsqueda y recorrida en un árbol binario.
    3. GNode : Arboles de orden n.
      1. Agregar nodos a un árbol.
      2. Eliminar Vs. desvincular.
      3. Información sobre los nodos.
      4. Buscar en el árbol y recorrerlo.
    4. Caches
      1. Creación del caché.
      2. Gestión de los datos del caché.
      3. Destrucción del caché.
  11. GLib Avanzado
    1. Hilos en Glib.
    2. UTF-8: las letras del mundo.
      1. Unicode: Una forma de representación (UCS).
      2. Qué es UTF-8.
      3. Conversiones del juego de caracteres.
        1. Nombres de archivos y UTF-8.
        2. Configuración local de caracteres y UTF-8.
      4. Otras funciones relacionadas con conversión de cadenas.
    3. Como hacer plugins.
      1. Abrir una biblioteca.
      2. Obtener un símbolo determinado.
      3. Preparar el plugin.
      4. Ejemplo de uso de GModule.

La biblioteca GLib es una de las más importantes que existen en GNOME. Esta biblioteca es, junto a la biblioteca GTK+, el pilar sobre el que se sustentan todas las aplicaciones.

Tipos de datos de GLib

Dentro de la biblioteca GLib está implementada una serie de tipos de datos que facilita, si cabe, el tratamiento de los datos y además tiene la propiedad de mejorar la portabilidad de los programas. Los tipos que se usan en GLib no son muy diferentes de los utilizados en el estándar de C, por lo cual no resultará complicado familiarizarse con ellos. A continuación se podrá observar una tabla de correspondencia entre los tipos de GLib contra los de estándar de C.

Tabla 6-1.1. Tipos de GLib que corresponden con los del estándar C.

Tipos GLib.

Tipos estándar C.

gchar

char

gint

int

gshort

short

glong

long

gfloat

float

gdouble

double

Como se ha demostrado en la tabla, los tipos de GLib no difieren en gran medida de los utilizados al programar con los tipos estándar de C, aunque es altamente recomendable la familiarización con estos tipos si se desea trabajar con la plataforma GNOME. GLib también posee una serie de tipos que hacen más sencillo el tratamiento de datos. Algunos tienen su correspondencia en C, pero facilitan su utilización; otros sirven para mantener el mismo tamaño de datos de una plataforma a otra y, finalmente, hay otros que no tienen correspondencia con el estándar de C, pero que resultarán bastante útiles.

Tabla 6-1.2. Tipos de GLib que facilitan el uso de tipos del estándar C.

Tipos de GLib

Tipos del C estándar.

Definición

gpointer

void *

gpointer es un puntero sin tipo, que luce mejor que escribir void *.

gconstpointer

gconstpointer es un puntero a una constante. Los datos a los que apuntan no deberían ser cambiados. Este tipo suele usarse en los prototipos de funciones para indicar que el valor al que apunta la función no debería ser cambiado en el interior de la función.

guchar

unsigned char

Como se puede observar, estos tipos son idénticos a los tipos de C que carecen de signo, pero con la ventaja de que son más visuales y fáciles de usar.

guint

unsigned int

gushort

unsigned short

gulong

unsigned long

Tabla 6-1.3. Tipos de GLib que aseguran el tamaño del dato entre plataformas.

Tipo

Rango de tamaño del dato.

gint8

-128 a 127

guint8

0 a 255

gint16

-32.768 a 32.767

guint16

0 a 65535

gint32

-2.147.483.648 a 2.147.483.647

guint32

0 a 4.294.967.295

gint64

-9.223.372.036.854.775.808 a 9.223.372.036.854.775.807

guint64

0 a 18.446.744.073.709.551.615

Tabla 6-1.4. Tipos de GLib nuevos que no están en el estándar de C.

Tipos de GLib

Definición

gboolean

Este tipo es booleano y sólo contendrá los valores TRUE o FALSE.

gsize

Es un entero de treinta y dos bits sin signo que sirve para representar tamaños de estructuras de datos.

gssize

Es un entero de 32 bits con signo, que sirve para representar tamaños de estructuras de datos.

Quizás, la existencia de tipos tales como gint8, gint16, gsize, etcétera, pudiese sugerir mayor complejidad a la hora de aprender a utilizar la biblioteca GLib. Sin embargo es importante denotar que cuando se programa mediante esta biblioteca, los tipos más utilizados son char, int, double, etc., aunque ahora se antepondrá una letra "g" a cada tipo.

Mensajes de salida

Mensajes de salida.

GLib implementa una serie de funciones para el envío de mensajes al exterior del programa. Al igual que en el ANSI C se dispone de la mítica función printf (), GLib posee una función que se asemeja en todo menos en el nombre; esta función es gprint () y su prototipo de función es:

void g_print (const gchar *format, ...);

Como se puede ver, el uso de g_print () es idéntico al de printf (). Es una función que recibe como parámetros un formato y las variables que se usan dentro de las características del formato, al igual que printf ().

Ejemplo 6-2.1. Mensajes de salida.

   1 /* ejemplo del uso de g_print */
   2 #include <glib.h>
   3 int
   4 main (int argc, char *argv[])
   5 {
   6         gint numero = 1;
   7         gchar *palabra = "hola";
   8         g_print ("glib te saluda : %s\nglib te dice un número : %d\n", palabra,
   9                  numero);
  10         return 0;
  11 }

Además, GLib provee de un método para dar formato a todos los mensajes de salida que se emitan con la función g_print (), por lo que se podría decir que, cada vez que se envía un mensaje con formato a g_print (), éste debe salir por pantalla con algún tipo de mensaje adicional. Esto es debido a la función g_set_print_handler (), que tiene como parámetros un puntero a función. Esta función, que es representada por el puntero a función, será definida por el programador y, con ella, se podrá decir qué mensaje adicional queremos que se vea. Para entenderlo mejor, se mostrará a continuación la sinopsis de esta función, así como un ejemplo.

GPrintFunc g_set_print_handler (GPrintFunc funcion);

Esta es la sinopsis de la función g_set_print_handler (), que recibirá como parámetro un puntero a función. Esta función podrá tener el siguiente aspecto

void (* GPrintFunc) (const gchar * cadena );

Éste sería el aspecto que ha de tener la función que se le pasará como parámetro a g_set_print_handler. El siguiente ejemplo ha sido concebido con la intención de facilitar su comprensión.

Ejemplo 6-2.2. Personalización de mensajes de salida.

   1 /* ejemplo de cómo añadir información a los mensajes de g_print */
   2 #include <glib.h>
   3 
   4 void funcion_personal (const gchar *cadena);
   5 
   6 /* funcion_personal es la función que le pasaremos a
   7    g_set_print_handler como parámetro */
   8 void
   9 funcion_personal (const gchar *cadena)
  10 {
  11         printf ("** Soy tu programa y te digo ** ");
  12         printf (cadena);
  13 }
  14 
  15 int
  16 main ()
  17 {
  18         /* g_print normal */
  19         g_print ("Soy un mensaje normal de salida\n");
  20 
  21         /* g_print personalizado */
  22         g_set_print_handler (funcion_personal);
  23         g_print ("mensaje de salida personalizado\n");
  24         return 0;
  25 }

GLib también provee de una función para comunicar mensajes de salida por la salida de errores estándar (stderr). Para el envío de mensajes existe la función g_printerr que funciona exactamente igual a g_print. Existe también una función como g_set_print_handler, llamada g_set_printerr_handler que funciona igual que la anterior.

Se puede ver, en conclusión, que el formateo personalizado de los mensajes de salida en GLib es bastante simple.

Funciones de depuración.

La depuración de un programa siempre es un trasiego por el cual todo programador, ya sea principiante o avanzado, ha de pasar. Y para hacer la vida más fácil al programador de GNOME, GLib nos ofrece una serie de funciones que harán más sencilla la depuración del código.

Estas funciones son útiles para comprobar que se cumple una serie de condiciones lógicas delimitadas por el programador. En caso de no ser cumplida, GLib emitirá un mensaje por consola, explicando que la condición que el programador ha puesto no se ha cumplido e indicando el archivo, la línea y la función en las que se ha roto la condición.

#define g_assert ( expresion );

La primera función de este tipo que se abordará es g_assert. Esta función recibe como parámetro un enunciado lógico y comprueba su validez. Para ejemplificar su uso, se expondrá un caso en el que a una función se le pase un puntero. Y se pondrá g_assert (ptr != NULL) para certificar que ese caso no se dé nunca. De hecho, si se diese tal caso, la función g_assert comprobaría que esa expresión no es válida y mandaría interrumpir la ejecución del programa. Para tener más claro este concepto, obsérvese la ejecución del siguiente ejemplo:

Ejemplo 6-2.3. Depuración de programas con g_assert.

   1 /* ejemplo del uso de g_assert */
   2 #include <glib.h>
   3 void
   4 escribe_linea (gchar *cadena)
   5 {
   6         /* si a la función se le pasa un puntero a NULL
   7            el programa cesara en la ejecución */
   8         g_assert (cadena != NULL);
   9         g_print ("%s\n", cadena);
  10 }
  11 
  12 int
  13 main ()
  14 {
  15         gchar *cadena = "esto es una cadena";
  16         gchar *no_es_cadena = NULL;
  17         /* como no se pasa un NULL sino una cadena, se escribirá
  18            la cadena "esto es una cadena" por pantalla */
  19         escribe_linea (cadena);
  20         /* como se le pasa un NULL el assert de la función
  21            escribe_linea surtirá efecto */
  22         escribe_linea (no_es_cadena);
  23         return 0;
  24 }

Si se compila este programa, en la salida del mismo, se podrá observar que cuando entra por segunda vez en la función escribe_linea, cesa la ejecución del programa. Además, por la consola saldrá un mensaje similar al siguiente.

        esto es una cadena

        ** ERROR **: file ejemplo.c: line 9 (escribe_linea): assertion failed: (cadena != NULL)
        aborting...

El mensaje muestra por pantalla información del archivo, de la línea y de la función en las que se viola la condición por la cual g_assert ha parado la ejecución. También indica qué expresión se ha incumplido, ya que pudiese darse el caso de tener varios g_assert en una misma función.

Quizá un método tan drástico como parar la ejecución del programa no sea lo más adecuado. Es probable que sólo se busque algo que termine la ejecución de la función y avise que una condición no ha sido cumplida. Para estos casos existen varias macros que facilitarán mucho la vida de los programadores. Estas macros son:

#define g_return_if_fail ()( expresion );

#define g_return_val_if_fail ()( expresion , val );

Ambas macros son similares en su funcionamiento, ya que comprueban si la expresión lógica que se les pasa como primer parámetro es verdadera o falsa. En el caso de ser falsa, aparecerá por consola un mensaje explicando que esa condición no ha sido cumplida. Esto es muy parecido a lo que hacía g_assert () la diferencia estriba en que estas dos funciones no finalizan la ejecución del programa, sino que simplemente hacen un retorno dentro de la propia función y, en consecuencia, el resto del programa seguirá ejecutándose.

Como previamente se mencionó, estas macros son similares. La diferencia radica en que la segunda, g_return_val_if_fail, hace que la función devuelva el valor que se le pasa como segundo parámetro.

Obviamente, g_return_if_fail se usará en funciones que no devuelvan ningún valor y g_return_val_if_fail en funciones que devuelvan algún valor. Para entender mejor estas funciones obsérvese el siguiente ejemplo.

Ejemplo 6-2.4. Depuración de programas con g_return_if_fail y g_return_val_if_fail.

   1 /* ejemplo del uso de g_return_if_fail y f_return_val_if_fail */
   2 #include <glib.h>
   3 
   4 void
   5 escribe_linea (gchar *cadena)
   6 {
   7         g_return_if_fail (cadena != NULL);
   8         g_print ("%s\n", cadena);
   9 }
  10 
  11 /* escribe línea 2 devolverá
  12    TRUE si la ha escrito bien
  13    FALSE si incumple la expresión */
  14 gboolean
  15 escribe_linea2 (gchar *cadena)
  16 {
  17         g_return_val_if_fail (cadena != NULL, FALSE);
  18         g_print ("%s\n", cadena);
  19         return  TRUE;
  20 }
  21 
  22 int
  23 main ()
  24 {
  25         gchar *cadena = "esto es una cadena";
  26         gchar *no_es_cadena = NULL;
  27         gboolean resultado ;
  28         escribe_linea (cadena);
  29         resultado = escribe_linea2 (cadena);
  30         if (resultado == TRUE)
  31                 g_print ("\n-- escribe_linea2 ha devuelto TRUE --\n");
  32         escribe_linea (no_es_cadena);
  33         resultado = escribe_linea2 (no_es_cadena);
  34         if (resultado == FALSE)
  35                 g_print ("\n-- escribe_linea2 ha devuelto FALSE --\n");
  36         return 0;
  37 }

Y, por consiguiente, la salida de este programa será similar a la siguiente:

esto es una cadena
esto es una cadena

-- escribe_linea2 ha devuelto TRUE --

** (process:1054): CRITICAL **: escribe_linea: assertion `cadena != NULL' failed

** (process:1054): CRITICAL **: escribe_linea2: assertion `cadena != NULL' failed

-- escribe_linea2 ha devuelto FALSE --

Como se puede ver, su ejecución también ofrece información útil, como el identificador del proceso al cual pertenece el mensaje y al igual que en el caso de la función g_assert, entrega como parte de la salida, la función en la cual no se ha cumplido la expresión y la condición que se viola, salvo que a diferencia de g_assert, esta vez no detiene la ejecución del programa.

Estas son las funciones para depuración que se pueden encontrar dentro de la biblioteca GLib. Aunque no son las únicas, sí son las más comunes.

Funciones de registro.

GLib tiene cuatro macros básicas para el tratamiento mensajes, por niveles de importancia. Estas macros permiten al programador realizar un registro del recorrido del programa especificando el nivel de importancia de los sucesos acontecidos dentro del mismo.

Estas cuatro macros reciben parámetros de la misma forma que lo hacían g_print o printf. De este modo, no sólo se tendrá un registro por niveles, sino que, además, se podrá especificar un texto preformateado. De este modo, los mensajes pueden representar por pantalla el contenido de variables o cualquier otra información que nos pudiera ser útil. Las cuatro macros de las que estamos hablando son :

#define g_message (...);

#define g_warning (...);

#define g_critical (...);

#define g_error (...);

Como se puede observar, las cuatro macros tienen nombres bastante intuitivos. Se puede notar, por los nombres de las funciones, de que se encuentran ordenadas de menor a mayor importancia, siendo g_message menos importante que g_error. A continuación se explicará lo que hace cada una de ellas.

  • g_message es una macro que emite un mensaje normal con la información que se le ha pasado como parámetro. La información que emite esta macro no tiene por que estar asociada a un error en el programa. Su principal aplicación es el envío de mensajes con la información que se crea conveniente en el proceso de realización del programa.

  • g_warning emite un mensaje de aviso con la información que le ha sido pasada como parámetro. Esta información puede servir para ser avisados de situaciones peligrosas que se podrían dar en el programa.

  • g_critical cumple la misma función que g_warning, sólo que éste además tiene la posibilidad de abortar el programa usando la función g_log_set_always_fatal().

  • g_error emite el mensaje que le haya sido pasado como argumento y además aborta irremediablemente la ejecución del programa. Esta macro se usa para obtener información de donde ha sucedido el error irrecuperable con la seguridad de que, a causa de este error, el programa terminara abortando su ejecución de todas maneras.

Para una mejor comprensión se muestra el siguiente ejemplo. En él se han definido cuatro funciones, cada una de las cuales emite un mensaje diferente. En este ejemplo no se hace que g_critical tenga capacidad de abortar el programa porque si no, no veríamos como funciona g_error.

Ejemplo 6-2.5. Uso de mensajes de registro.

   1 /* ejemplo del uso de g_warning, g_critical, ... */
   2 #include <glib.h>
   3 void
   4 funcion_con_mensaje (){
   5         g_message ("Yo soy solamente un mensaje de aviso\n");
   6 }
   7 void
   8 funcion_con_aviso (){
   9         g_warning ("Esto es un aviso, algo puede ir mal\n");
  10 }
  11 void
  12 funcion_con_aviso_critico (){
  13         g_critical ("Esto se pone difícil :( \n");
  14 }
  15 void
  16 funcion_con_un_error (){
  17         g_error ("3, 2, 1... acabo de morirme. RIP\n");
  18 }
  19 main (){
  20         funcion_con_mensaje ();
  21         funcion_con_aviso ();
  22         funcion_con_aviso_critico ();
  23         funcion_con_un_error ();
  24 }

Aunque este ejemplo no tiene mucha funcionalidad, sirve para ver en acción a las cuatro macros en un mismo programa y da la posibilidad de contemplar en la salida de este programa los mensajes que envía a la consola. Si se ejecuta el ejemplo su salida sería:

** Message: Yo soy solamente un mensaje de aviso


** (process:13070): WARNING **: Esto es un aviso, algo puede ir mal


** (process:13070): CRITICAL **: Esto se pone difícil :( 


** ERROR **: 3, 2, 1... acabo de morirme. RIP

aborting...
Aborted (core dumped)

Como se puede ver, todas las funciones y macros que se han mostrado en este apartado serán especialmente útiles a la hora de realizar un programa y posibilitarán una vía estandarizada y muy flexible para la emisión de información al exterior. Facilitando la corrección del código y haciendo más facil la tarea del programador.

Trabajar con cadenas

Manipular el contenido de una cadena.

La manipulación de cadenas es una tarea muy común en la vida diaria de un desarrollador. El desarrollador necesita realizar tareas como duplicar cadenas, transformarlas en mayúsculas o verificar si algún carácter corresponde a un tipo en especial. Para este tipo de tareas, esta biblioteca dispone de una serie de funciones cuyo uso no dista mucho de las ya desarrolladas en la Libc.

Duplicación de cadenas.

Si lo que se desea es duplicar cadenas, las funciones g_strdup y g_strndup resolverán esa necesidad. Las dos funciones realizan el mismo trabajo: duplican la cadena que se le ha pasado como parámetro. La diferencia radica en que la segunda limita el número de caracteres que devuelve a un número que le es indicado por un segundo parámetro de la función.

gchar * g_strdup (const gchar * cadena_para_duplicar );

gchar * g_strndup (const gchar * cadena_para_duplicar , gsize longitud_maxima);

Verificación de caracteres.

El título se refiere al conjunto de funciones capaces de responder a preguntas como: ¿Este carácter es un dígito? o ¿este carácter es una letra?... Este tipo de funciones es muy usado en el tratamiento de cadenas y su funcionamiento es muy simple. Estas funciones reciben un carácter y devuelven cierto o falso. La siguiente tabla explica cuáles son esas funciones y cuál es la verificación que realizan. Se recomienda la observación del prototipo de la función g_ascii_isalnum, usado como ejemplo para una mejor comprensión de la tabla, ya que todas las funciones comparten el mismo prototipo.

gboolean g_ascii_isalnum (gchar carácter );`

Tabla 6-3.1. Funciones de verificación de caracteres.

Nombre de función

Es cierto si...

g_ascii_isalnum

Es un carácter alfanumérico.

g_ascii_isalpha

Es un carácter del alfabeto.

g_ascii_iscntrl

Es un carácter de control.

g_ascii_isdigit

Es un dígito.

g_ascii_isgraph

Es un carácter imprimible y no es un espacio.

g_ascii_islower

Es una letra minúscula.

g_ascii_isprint

Es un carácter imprimible.

g_ascii_ispunct

Es un carácter de puntuación.

g_ascii_isspace

Es un espacio.

g_ascii_isupper

Es una letra mayúscula.

Copia y concatenación de cadenas.

Otra acción típica para el programador es la copia de una cadena o la concatenación de de múltiples cadenas. Para realizar esta tarea se puede utilizar la función g_stpcpy. Esta función desempeña la tarea de copiar una cadena que se le pase como parámetro a una cadena destino, también pasada como parámetro. Esta función tiene la particularidad de devolver un puntero a la última posición de la cadena destino, de tal modo que, si se quisiera concatenar cadenas, sólo se tendría que pasar el puntero devuelto por el primer g_stpcpy al segundo como si fuera el destino. Así el segundo g_stpcpy entendería que ha de copiar la segunda cadena al final de la primera que se ha copiado anteriormente y así sucesivamente. Siguiendo este método, se podría concatenar una cadena tras otra.

gchar * g_stpcpy (gchar * cadena_destino , const gchar * cadena_origen );

GString : la otra manera de ver una cadena.

GString es un TAD (tipo abstracto de datos) que implementa GLib. Este TAD es un buffer de texto que tiene la capacidad de expandirse, realojando memoria automáticamente, sin que el programador tenga que actuar. Esto es realmente útil para el trabajo con cadenas, porque cuando se reserva memoria para texto y, más adelante, es insuficiente, el programador tiene que tomarse la molestia de reservar más. Por eso GLib implementa GString, que facilita enormemente el manejo de cadenas sin necesidad de estar pendientes de su gestión de memoria.

Para empezar a estudiar este TAD, lo primero que se debe conocer es la estructura de datos sobre la que esta diseñada y después se estudiaran las funciones que interactúan con esa estructura.

   1                   struct GString
   2                   {
   3                       gchar  *str;                                      (1)
   4                       gsize len;                                        (2)
   5                       gsize allocated_len;
   6                   };

(1)

  • En este puntero a una cadena de caracteres, es donde se almacenará el contenido de la cadena que se especifique. De este modo, si en algún momento se necesita el contenido de la cadena, sólo es necesario acudir a este campo.

(2)

  • Este campo hace referencia a la longitud total de la cadena que está alojada en el campo str de la estructura GString. De este modo, si en str está alojada la cadena "hola", el campo len contendrá el número 4.

Ahora se ha observado cómo es la estructura GString. El siguiente paso será ver las funciones que trabajan sobre GString con el fin de sacarle el mayor partido a este tipo abstracto de datos.

Cómo se aloja una cadena con GString.

Alojar una cadena con GString es extremadamente sencillo e intuitivo. Para ello se utilizará la función g_string_new (), que tiene la forma:

GString * g_string_new (const gchar * cadena_inicial );

Esta función recibe como parámetro una cadena y devuelve la estructura GString. De este modo, dentro de la estructura GString, se tendrá toda la información y, como se puede observar, no se ha sido necesario realizar movimiento alguno para alojar memoria.

Ejemplo 6-3.1. Crear una cadena con GString.

   1 /* ejemplo del uso de g_string_new */
   2 #include <glib.h>
   3 
   4 int
   5 main ()
   6 {
   7         
   8         GString *cadena ;
   9         
  10         cadena = g_string_new ("!Qué fácil es hacer una cadena!");
  11         
  12         g_print ("la cadena que hemos alojado : %s\n", cadena->str);
  13         g_print ("la longitud de la cadena es : %d\n", cadena->len);
  14         return 0;
  15 }

Como se puede ver en la ejecución de este ejemplo, la creación de una cadena con GString es muy sencilla. Pero, además, GLib proporciona otra función para crear cadenas. La sinopsis de esta función es:

GString * g_string_new_len (const gchar * cadena_inicial,
                            gssize longitud_inicial );

La ventaja que tiene el uso de g_string_new_len es que permite especificar una longitud inicial y esto trae consigo que la cadena introducida no tiene necesariamente que estar terminada con el carácter NULL ("\0") y, además, puede tener varios caracteres NULL embebidos dentro de la cadena en cuestión.

Cómo puedo liberar un GString

Siendo GString una estructura, lo suyo sería liberar uno a uno todos los campos pero, gracias a la implementación que ofrece GLib, se dispondrá de una función que ayuda con la liberación de memoria que se ha reservado con la creación de un GString. La sinopsis de esta función sería:

gchar * g_string_free (GString * cadena , gboolean liberar_contenido_cadena );

Con la función g_string_free, se podrá liberar el GString que se entrega como primer argumento a la función. Además, esta función también da la posibilidad de liberar o no la cadena que estaría dentro de GString. Y esto se conseguiría si como segundo parámetro se le pasara el booleano TRUE. En caso contrario, si se le pasara un booleano FALSE, la cadena de caracteres no sería liberada.

Añadir, insertar y borrar cadenas en un GString.

Con los conocimientos necesarios para crear y liberar este tipo de datos, ha llegado el momento de aprender a trabajar con la funciones que tratan la información que aloja y, para empezar, se aprenderá como añadir texto a una cadena creada como GString. A la hora de añadir texto, puede interesar añadir texto al final o al principio de la cadena y se podrá hacer con GString. Las funciones que se verán ahora son exactamente iguales, salvo que contienen las palabras append o prepend, que indican que la función añade el texto al final o al principio respectivamente.

GString * g_string_append (GString * cadena,
                           gchar * cadena_para_añadir_al_final );

GString * g_string_prepend (GString * cadena,
                            gchar * cadena_para_añadir_al_principio );

Estas son las primeras funciones para el añadido de texto que se repasarán. La primera, g_string_append, se encargará de añadir el texto pasado como parámetro al final de la cadena contenida dentro del GString y la segunda, g_string_prepend, se encargará de añadir el texto al principio.

Con el siguiente ejemplo, se demostrará el uso práctico de estas funciones.

Ejemplo 6-3.2. Añadir cadena a un GString.

   1 /* ejemplo del uso de g_string_append y g_string_prepend */
   2 #include <glib.h>
   3 
   4 int
   5 main ()
   6 {
   7 
   8         GString *cadena ;
   9         
  10         cadena = g_string_new ("*");
  11         cadena = g_string_append (cadena, "| texto añadido al final");
  12         cadena = g_string_prepend (cadena, "texto añadido al principio |");
  13         
  14         g_print ("\n%s\n", cadena->str);
  15         return 0;
  16 }

Una vez que se sabe añadir texto por delante y por detrás de la cadena contenida dentro del GString, el siguiente paso natural sería aprender cómo se pueden introducir datos dentro de las cadenas, es decir, insertar una cadena. En el caso que se tenga un GString con una cadena. Con la función g_string_insert se puede insertar, en la posición elegida, la cadena que desee. Como se puede observar, en la sinopsis de la función y en el siguiente ejemplo, Lo único que se debe hacer es indicar la posición en la cual se quiere insertar la cadena y la cadena que se insertará.

GString * g_string_insert (GString * cadena, gssize posicion,
                           gchar * cadena_para_insertar );

Ejemplo 6-3.3. Insertar una cadena a un GString.

   1 /* ejemplo del uso de g_string_insert */
   2 #include <glib.h>
   3 
   4 int
   5 main ()
   6 {
   7         
   8         GString *cadena ;
   9         gchar *cadena_para_insertar = "muy muy ";
  10         
  11         cadena = g_string_new ("El día esta soleado");
  12         cadena = g_string_insert (cadena, 13, cadena_para_insertar);
  13         
  14         g_print ("\n%s\n", cadena->str);
  15         
  16         g_string_free (cadena, TRUE);
  17         return 0;     
  18 }

Con las funciones mostradas anteriormente, se puede añadir información a un GString. Si lo que se desea es borrar, se cuenta con la función g_string_erase. Sólo hay que indicar la posición desde la que quiere empezar a borrar y el número de caracteres que desea borrar desde esa posición.

GString * g_string_erase (GString * cadena, gssize posicion,
                          GString * numero_de_caracteres_que_desea_borrar );

Ejemplo 6-3.4. Borrar una cadena a un GString.

   1 /* ejemplo del uso de g_string_erase */
   2 #include <glib.h>
   3 
   4 int
   5 main ()
   6 {
   7         
   8         GString *cadena ;
   9         
  10         cadena = g_string_new ("Esto es una cadena --esto sera borrado--");
  11         cadena = g_string_erase (cadena, 19, 21);
  12         
  13         g_print ("\n%s\n", cadena->str);
  14         g_string_free (cadena, TRUE);
  15         return 0;
  16 }

Introducir texto preformateado a un GString.

Esta es una de las aplicaciones más interesantes que se le puede dar a un GString. Este tipo de datos permite trabajar con texto preformateado, como cuando se trabaja con un printf. Esta opción siempre es interesante, pues una vez que se tenga ese texto dentro del GString, se podrán aprovechar las múltiples facetas que posee este tipo.

La siguiente función, g_string_printf, recordará a la ya conocida sprintf() de C estándar; la diferencia está en que el buffer tiene la capacidad de crecer automáticamente. Otra cosa a tener en cuenta en el uso de esta función es que cuando se usa, el contenido anterior existente (si existiese) es destruido.

void g_string_printf (GString * cadena , const gchar formato , ... );

Ejemplo 6-3.5. Crear texto preformateado con GString.

   1 /* ejemplo del uso de g_string_printf */
   2 #include <glib.h>
   3 
   4 int
   5 main ()
   6 {
   7         
   8         GString *cadena ;
   9         gchar *frase = "lo bueno, si breve, dos veces bueno" ;
  10         gint numero = 7 ;
  11         
  12         cadena = g_string_new ("");
  13         g_string_printf (cadena, "FRASE TIPICA : %s\nNUMERO DE LA SUERTE : %d",
  14                          frase, numero);
  15         
  16         g_print ("%s\n",cadena->str);
  17         g_string_free (cadena, TRUE);
  18         return 0;       
  19 }

GLib también dispone de una función similar a la anterior. La única diferencia estriba en que esta función añade el texto preformateado. Es decir, si se tuviera un texto dentro del GString, el texto formateado se añadiría detrás del texto existente. Esto puede resultar útil en el sentido de que la anterior función destruía el contenido anterior y puede que eso no le convenga.

void g_string_append_printf (GString * cadena , const gchar formato , ... );

Ejemplo 6-3.6. Añadir texto preformateado con GString.

   1 /* ejemplo del uso de g_string_append_printf */
   2 #include <glib.h>
   3 
   4 int
   5 main ()
   6 {
   7         
   8         GString *cadena ;
   9         gchar *frase = "lo bueno, si breve, dos veces bueno" ;
  10         gint numero = 7 ;
  11         
  12         cadena = g_string_new ("Línea ya existente\n");
  13         g_string_append_printf (cadena,
  14                                 "FRASE TIPICA : %s\nNUMERO DE LA SUERTE : %d",
  15                                 frase, numero);
  16         g_print ("%s\n",cadena->str);
  17         g_string_free (cadena, TRUE);
  18         return 0;     
  19 }

Otras funciones útiles para el manejo de GString.

En las secciones anteriores se han explicado las funciones más comunes con las que poder manejar una estructura del tipo GString, pero estas funciones no son las únicas que existen para trabajar con este tipo de datos. En la siguiente sección, se explicarán otras que, aunque no son tan fundamentales, pueden resultar útiles.

Para empezar, puede que no siempre sea de gran interés insertar o añadir una cadena y sólo se quiera trabajar con un carácter. Para ello se dispone de estas funciones que, aunque surten el mismo efecto que si se usase g_string_append y similares, con un sólo carácter, puede que estas funciones sirvan para facilitar la legibilidad del código en un momento dado.

GString * g_string_append_c (GString * cadena , gchar caracter );

GString * g_string_prepend_c (GString * cadena , gchar caracter);

GString * g_string_insert_c (GString * cadena, gssize posicion,
                             gchar * caracter );

Ejemplo 6-3.7. Añadir e insertar caracteres a un GString.

   1 /* ejemplo del uso de g_string_{insert|append|prepend}_c */
   2 #include <glib.h>
   3 
   4 int
   5 main ()
   6 {
   7         
   8         GString *cadena;
   9         gchar caracter ;
  10         
  11         cadena = g_string_new ("");
  12         
  13         caracter = 'c' ;
  14         cadena = g_string_append_c (cadena, caracter);
  15         caracter = 'a' ;
  16         cadena = g_string_prepend_c (cadena, caracter);
  17         caracter = 'b' ;
  18         cadena = g_string_insert_c (cadena, 1, caracter);
  19         
  20         g_print ("la cadena resultante es... %s\n",cadena->str);
  21         g_string_free (cadena, TRUE);
  22         return 0;     
  23 }

Otras acciones que pueden resultar interesantes son la capacidad de truncar un texto por el lugar que se desee y la capacidad de convertir todo el texto a mayúsculas o minúsculas. Pues para satisfacer este tipo de necesidades se dispone de g_string_truncate, cuya sintaxis se muestra a continuación.

GString * g_string_truncate (GString * cadena , gsize longitud );

longitud se refiere al número de caracteres desde el principio de la cadena que se desea permanezcan una vez terminada la función.

GString * g_string_up (GString * cadena );

GString * g_string_down (GString * cadena );

La primera función, g_string_up, es la que convierte a mayúsculas un texto íntegro del GString y la segunda, g_string_down la que lo convierte en minúsculas.

GQuarks.

Los Quarks son asociaciones entre una cadena de texto y un identificador numérico (gunint32). Ya sea proporcionando el GQuark o la cadena de texto es posible obtener la información relacionada el uno con el otro. Los Quarks son utilizados en los Datasets y en los Keyed Data List.

Para crear un Quark nuevo de una cadena de texto se usa g_quark_from_string () o g_quark_from_static_string (). Para encontrar la cadena de texto correspondiente a un GQuark se utiliza g_quark_to_string (). Y para encontrar el Quark de una cadena de texto se puede usar g_quark_try_string ().

GQuark g_quark_from_string (const gchar *string );

Obtiene el identificador GQuark de la cadena que se pase como parámetro. Si la cadena de texto no tiene asociado un GQuark, un nuevo GQuark es creado, usando una copia de la cadena de texto.

GQuark g_quark_from_static_string (const gchar *string );

Obtiene el identificador GQuark de una cadena de texto estática. Si la cadena de texto no tiene asociado un GQuark, un nuevo GQuark es creado, vinculándolo con la cadena.

Esta función es idéntica a g_quark_from_string () excepto que si un GQuark es creado la propia cadena de texto es usada en lugar que una copia. Esto ahorra memoria, pero solo puede usarse si la cadena de texto siempre existe. Esto puede usarse con cadenas de texto que están alojadas estáticamente en el programa principal, pero no con las alojadas en memoria en el módulo cargado dinámicamente.

const gchar * g_quark_to_string (GQuark quark );

Obtiene la cadena de texto asociada con un GQuark.

GQuark g_quark_try_string (const gchar * string );

Obtiene el GQuark asociado con la cadena de texto. Regresa el GQuark asociado con la cadena de texto, o 0 si no hay un GQuark asociado con la cadena de texto.

GQuark es usado junto con Keyed Data Lists y Datasets, que son listas para organizar elementos que son accedidos por medio de una cadena de texto o por medio de un identificador GQuark, y Datasets es utilizado para asociar grupos de datos con posiciones de memoria.

Jugando con el tiempo

Funciones de manejo de fechas y horas.

GLib proporciona una API completa para poder programar, utilizando fechas dentro de nuestras aplicaciones de forma sencilla. El objetivo fundamental es dar soporte a las fechas, cuya programación puede ser pesada sin estas funciones, aunque también se proporcionan algunos métodos para relacionar tiempos y fechas. Para ello, GLib dispone de la estructura GDate, la cual es la estructura que incorpora toda la información sobre las fechas y, además, una serie de funciones que interactúan con esa estructura.

Antes de empezar, han de aclararse unos conceptos sobre la expresión del tiempo, en cuanto al formato de las fechas se refiere. Hay dos formas de expresar las fechas: día-mes-año y formato Juliano; este último es simplemente el número de días que han transcurrido desde una fecha de referencia (en el caso de GLib, dicha fecha es el 1 de Enero del año 1).

Creación de una nueva fecha.

El primer paso para poder utilizar una fecha es crearla. Para crear una fecha se dispone de varias funciones, la primera, g_date_new, permite crear una estructura GDate sin ningún valor. Esta función resultará útil más adelante cuando se vea como ajustar la estructura GDate a una determinada fecha. Las dos siguientes permiten crear una fecha pero, al contrario que la anterior, que sólo crea la estructura, éstas añaden además la fecha: la función g_date_new_julian, que nos permite crear una fecha utilizando el formato juliano y, por último, g_date_new_dmy, a la que se le pasa un día, mes y año para crear el GDate.

GDate * g_date_new ( void );

GDate * g_date_new_julian (guint32 dia_juliano );

GDate * g_date_new_dmy (GDateDay dia , GDateMonth mes , GDateYear año );

En esta última función, es conveniente que se examinen los tipos de datos GDateDay, GDateMonth, GDateYear.

El tipo de datos GDateDay es un entero (entre 1 y 31) que representa el día del mes. La constante G_DATE_BAD_DAY representa un día inválido del mes.

El tipo GDateMonth es un tipo enumerado, por tanto se puede referir a él con un número del 1 al 12, refiriéndose a los meses de enero a diciembre o también con los elementos G_DATE_JANUARY, G_DATE_FEBRUARY, etc. La constante G_DATE_BAD_MONTH representa un mes inválido.

Y, por último, está el tipo GDateYear. Este tipo es, en realidad, un entero; por tanto, para representar el año, basta con poner el número del año deseado.

Midiendo intervalos de tiempo con GTimer.

Dentro del mundo de la programación, medir el tiempo entre dos instantes dentro del programa es una actividad bastante común. Por ejemplo, en programas que transfieren un archivo y miden la velocidad de transferencia o en programas de instalación, para mostrar por pantalla el tiempo de instalación usado. La medida del tiempo es tan necesaria como, a veces, dependiente de la plataforma para la que se desarrolle. Por eso, GLib provee la estructura GTimer y sus funciones asociadas, gracias a lo cual, se podrá calcular intervalos de tiempo con una resolución del orden de los microsegundos.

Descripción de las funciones para manejar GTimer.

La estructura GTimer es opaca para el programador. Todo lo necesario para gestionar GTimer se realiza con las funciones que se describen a continuación, lo que quiere decir que no se mostrará en el libro la estructura en detalle como se hizo en otras secciones, ya que ni el mismo programador la ve. Sólo se interactúa con ella a través de funciones, como ya se ha dicho.

Para empezar, lo primero que se ha de hacer, como en la gran mayoría de estructuras de GLib, es inicializar o crearla. Para este fin se dispone de la función g_timer_new. Esta función se encarga única y exclusivamente de inicializar la estructura GTimer.

GTimer * g_timer_new ( void );

Una vez inicializada la estructura GTimer, lo siguiente es ver qué se puede hacer con ella. Como se mencionó previamente, esta estructura está dedicada a medir intervalos de tiempo y se propuso el ejemplo de un programa en el cual se transfiere un archivo y mide el tiempo de transferencia. Si se abstrae el problema en sí y se fija la atención solamente en lo que tendría que hacer el programa para medir los tiempos, se llegará a la conclusión que se requiere una función que arranque el contador de tiempo para que, cuando empezase la transferencia, se marcase de alguna manera el tiempo en el que se empezó a transferir, y una función que pare el contador cuando el archivo acabe de transferirse. Y, por último, una función con la cual se pueda consultar el tiempo en un momento dado, una vez arrancado el contador. Pues todas esas funciones están implementadas en GTimer.

Para empezar, existe la función g_timer_start, que tiene la función de marcar el tiempo de inicio en el que se ha arrancado el contador. Luego está g_timer_stop que marca el momento final de la medida de tiempo. Y por último g_timer_elapsed, que tiene un carácter más complejo, pero que resulta sencillo de comprender. Esta función devuelve el número de segundos que hayan transcurrido y, si se desea más precisión el puntero a microsegundos devolverá el número de microsegundos. Ahora bien, si se ha iniciado el contador pero aún no se ha detenido con g_timer_stop, obtiene el tiempo transcurrido desde que se inició el contador. Si se ha llamado a g_timer_stop, obtiene el tiempo transcurrido entre el inicio del contador de tiempo y el momento en que fue detenido.

void g_timer_start ( GTimer * contador );

void g_timer_stop ( GTimer * contador );

gdouble g_timer_elapsed ( GTimer * contador , gulong * microsegundos );

Ahora, en un contexto un poco más avanzado, GTimer también dispone de una función que reinicia el contador: g_timer_reset. Esta función es equivalente a llamar a g_timer_start si el contador ya está activo, es decir, reinicia el contador de tiempo poniendo como instante inicial, el momento de la llamada a esta función. En el caso de estar parado, lo pone a cero.

GTimer * g_timer_reset ( GTimer * timer );

Y, por ultimo, y dado que la estructura GTimer es una estructura opaca al desarrollador, necesitamos una función que libere todos los recursos de memoria que haya requerido la estructura, es decir, una función que libere de memoria la estructura GTimer. Para ello está g_timer_destroy.

GTimer * g_timer_destroy ( GTimer * timer );

Como unas líneas de código valen más que mil palabras, aquí se propone un ejemplo que aclara dudas sobre el uso de estas funciones si las hubiere.

Ejemplo 6-4.1. Ejemplo de uso de GTimer

   1 /* ejemplo del uso de GTimer */
   2 #include <unistd.h> /* contiene la función sleep */
   3 #include <glib.h>
   4 
   5 int
   6 main ()
   7 {
   8         GTimer *timer1, *timer2;
   9         gdouble elapsed;
  10         gulong usec;
  11         
  12         timer1 = g_timer_new (); /* Llama implícitamente a g_timer_start() */
  13         timer2 = g_timer_new ();
  14         
  15         g_print ("Duermo 5 segundos...");
  16         sleep (5);
  17         g_print ("\n\n");
  18         
  19         g_print ("Paro el timer 1...\n");
  20         g_timer_stop (timer1);
  21         
  22         elapsed = g_timer_elapsed (timer1, &usec);
  23         g_print ("Tiempo transcurrido timer 1: %gs %luus\n", elapsed, usec);
  24         
  25         elapsed = g_timer_elapsed (timer2, &usec);
  26         g_print ("Tiempo transcurrido timer 2: %gs %luus\n\n", elapsed, usec);
  27         
  28         g_print ("Duermo 1 segundo más...\n");
  29         sleep (1);
  30         
  31         elapsed = g_timer_elapsed (timer1, &usec);
  32         g_print ("Tiempo transcurrido timer 1 (igual que antes): \
  33                   %gs %luus\n", elapsed, usec);
  34         elapsed = g_timer_elapsed (timer2, &usec);
  35         g_print ("Tiempo transcurrido timer 2 (un segundo más): \
  36                   %gs %luus\n\n", elapsed, usec);
  37         
  38         g_print ("Reseteo el timer 1 (que esta parado).\n");
  39         g_timer_reset (timer1);
  40         g_print ("Reseteo el timer 2 (que esta activo).\n");
  41         g_timer_reset (timer2);
  42         
  43         g_print ("Duermo otro segundo más...\n");
  44         sleep (1);
  45         
  46         elapsed = g_timer_elapsed (timer1, &usec);
  47         g_print ("Tiempo transcurrido timer 1 \
  48                   (0, no se ha llamado a g_timer_start()): \
  49                   %gs %luus\n", elapsed, usec);
  50         
  51         elapsed = g_timer_elapsed (timer2, &usec);
  52         g_print ("Tiempo transcurrido timer 2 (1 segundo): \
  53                   %gs %luus\n\n", elapsed, usec);
  54         
  55         g_print ("Llamo a g_timer_start () con timer 1.\n");
  56         g_timer_start (timer1);
  57         
  58         g_print ("Y vuelvo a dormir, 2 segundos...\n");
  59         sleep (2);
  60         
  61         elapsed = g_timer_elapsed (timer1, &usec);
  62         g_print ("Tiempo transcurrido timer 1 (2 segundos): \
  63                   %gs %luus\n", elapsed, usec);
  64         elapsed = g_timer_elapsed (timer2, &usec);
  65         g_print ("Tiempo transcurrido timer 2 (3 segundos): \
  66                   %gs %luus\n\n", elapsed, usec);
  67         
  68         g_timer_destroy (timer2);
  69         g_timer_destroy (timer1);
  70         
  71         return 0;
  72 }

Miscelánea de funciones

Números aleatorios.

En determinadas ocasiones es necesario trabajar con valores aleatorios, por ejemplo a la hora de programar juegos. En los juegos, muchas veces, hace falta elegir al azar qué carta se va a levantar, dónde se va a poner la mina, etc. Para tomar este tipo de decisiones hacen falta valores aleatorios, es decir, al azar o que se acerquen mucho a ello. Debido a esta necesidad se ha implementado en GLib la biblioteca gran.h, que consiste en una estructura de datos GRand y un conjunto de funciones. Estas funciones sirven para inicializar la estructura GRand, darle valores iniciales y obtener números aleatorios de distintos tipos.

La referencia a los tipos en el párrafo anterior es en el sentido más informático de la palabra, porque todos son números aleatorios, con la diferencia de que en unos se obtiene un guint32, gdouble, gboolean (este último, no siendo número, sino un valor booleano). Después algunas funciones establecen rangos para los números. Un número aleatorio entre el X y el Y.

Cuando se habla de números pseudoaleatorios, hay que irremediablemente hablar de dos conceptos: PRNG (generador de números pseudo aleatorios) y "semilla".

Un PRNG no es más que una estructura de datos con algoritmos asociados que, a partir de un valor inicial, genera secuencias de números en un orden, aparentemente aleatorio o, lo que es lo mismo, al azar y es aparentemente ya que no es real; si se tuviera la tecnología y el tiempo suficiente, se podría encontrar un cierto orden lógico en las secuencias y predecirlas, pero eso es otro tema; a los efectos, serán considerados como realmente aleatorios.

En el caso de GLib, este PRNG, será GRand, que es una estructura que, aunque oculta a la visión del programador, es usada internamente por las funciones de la biblioteca grand.h.

El otro concepto es el de semilla. La semilla, no es más que un valor inicial mencionado con anterioridad. Un valor, de ser posible, bastante aleatorio en sí mismo. Para conseguir esta aleatoriedad, se le entrega este valor a la función correspondiente. En caso de que no se quiera pasar la semilla, el constructor de GRand, g_rand_new(), se encargará de obtener un valor aleatorio de /dev/urandom, si existe, o la hora actual en caso contrario.

Para usar este generador de números pseudo aleatorios, es necesario incluir la biblioteca grand.h.

   1 #include <grand.h>
   2 

Para obtener números aleatorios rápidamente se pueden usar las siguientes funciones:

void g_random_set_seed(guint32 semilla);

Con esta función se establece la semilla para la generación de aleatorios. Estableciendo esta semilla con un mismo número cada vez que se ejecute nuestro código, se obtendrá el mismo resultado para los números aleatorios que se generen. Si se desea que los valores aleatorios no coincidan nunca, basta con no establecer la semilla.

gboolean g_random_boolean();

Con esta función, se obtiene un booleano aleatorio.

guint32 g_random_int(void);

Se obtiene un gunt32 distribuido sobre el rango [0..2^32-1].

gint32 g_random_int_range(gint32 principio, gint32 end);

Se obtiene un gint32 distribuido sobre el rango [principio..fin-1].

gdouble g_random_double(void);

Se obtiene un gdouble distribuido sobre el rango [0..1].

gdouble g_random_double_range(gdouble principio, gdouble fin);

Se obtiene un gdouble distribuido sobre el rango [principio..fin].

En todos estos casos, internamente, se ha hecho uso de un tipo de dato: GRand. Si lo que se requiere es obtener una serie de números aleatorios reproducibles, será una mejor opción trabajar directamente con este dato.

La manera de hacerlo es crear primero un GRand y después trabajar con las funciones del tipo g_rand*.

GRand *g_rand_new(void);

Crea un nuevo GRand. Los números aleatorios necesitan una semilla para inicializarse, pero en esta función no se le entrega ninguna, así que se utilizará como semilla un valor de /dev/urandom, si existe, o la hora actual en caso contrario.

GRand *g_rand_new_with_seed(guint32 semilla);

En este caso se crea un GRand usando una semilla.

void g_rand_set_seed(GRand *rand, guint32 semilla);

Con esta función se establece una nueva semilla a un GRand.

void g_rand_free(GRand *rand);

Libera la memoria usada por un GRand.

gboolean g_rand_boolean(GRand *rand);

Devuelve el siguiente booleano aleatorio desde rand.

guint32 g_rand_int(GRand *rand);

Devuelve el siguiente guint32 aleatorio desde rand entre los valores [0..2^32-1].

gint32 g_rand_int_range(GRand *rand, gint32 principio, gint32 fin);

Devuelve el siguiente gint32 aleatorio desde rand entre los valores [principio..fin-1].

gdouble g_rand_double(GRand *rand);

Devuelve el siguiente gdouble aleatorio desde rand entre los valores [0..1].

gdouble g_rand_double_range(GRand *rand, gint32 principio, gint32 fin);

Devuelve el siguiente gdouble aleatorio desde rand entre los valores [principio..fin].

Con estas funciones se pueden obtener números o series de números pseudoaleatorios, de una manera rápida y fácil. Además con la ventaja añadida de la portabilidad que nos provee GLib.

Funciones de información sobre entorno.

Muchas veces, en el desarrollo de aplicaciones, es necesario conocer e interactuar con información del entorno en el cual se esté trabajando. Por ejemplo, en un momento dado, podría ser necesario conocer información del usuario como su nombre, su directorio de trabajo o el directorio donde se pueda almacenar información temporal. Para todas estas acciones e incluso algunas más, se dispone de las siguientes funciones.

gchar* g_get_user_name (void);

gchar* g_get_home_dir (void);

gchar* g_get_tmp_dir(void);

gchar* g_get_current_dir(void);

Después de haber visto la sintaxis de estas funciones no resulta muy difícil identificar para qué sirve cada una, pero por si queda alguna duda, la primera devolverá el nombre del usuario que esté ejecutando el programa en ese momento; la segunda, su directorio de trabajo; la tercera, el directorio temporal habilitado en su sistema para ese fin y la última función devolverá el directorio en el que se esté trabajando actualmente.

Estas funciones pueden resultar útiles, pero para aquellos que provengan del universo UNIX o GNU/Linux puede que no sea suficiente. Esto es debido a que, en este tipo de sistemas operativos, el uso de variables de entorno en sus diferentes shells es muy extendido y en un momento dado, podrá requerirse recurrir a ellas. Para ello existe la función g_getenv. La cual es realmente simple de utilizar ya que sólo requiere como parámetro la variable que se necesite y esta función devolverá su valor en una cadena.

gchar* g_getenv (const gchar * variable );

Ejemplo 6-5.1. Obtención de variables de entorno.

   1 /* ejemplo de como obtener información sobre las variables de entorno */
   2 #include <glib.h>
   3 
   4 int
   5 main ()
   6 {
   7         g_print("Yo soy el usuario %s\n", g_get_user_name());
   8         g_print("Vivo en %s\n", g_get_home_dir());
   9         g_print("Puedo escribir archivos temporales en %s\n", g_get_tmp_dir());
  10         g_print("Y ejecuto este programa desde %s\n", g_get_current_dir());
  11         g_print("Mi interprete de comandos es %s\n", g_getenv("SHELL"));
  12 
  13         return 0;
  14 }

Lo cual podría entregarnos un resultado similar a:

Yo soy el usuario basilio
Vivo en /home/basilio
Puedo escribir archivos temporales en /tmp
Y ejecuto este programa desde /home/basilio/Code/ejemplos
Mi interprete de comandos es /bin/bash

Bucles de ejecución

Los bucles de ejecución son estructuras que permiten realizar programas asíncronos, es decir, no bloqueantes; hacen posible ejecutar un bucle que permanece a la escucha de diversos eventos y que envía "señales" a distintas funciones que se registran como interesadas en determinados eventos.

De esta manera se pueden programar aplicaciones, que por ejemplo, realicen cierta función cada determinado tiempo, esto se logra mediante "alarmas", pero no solo se limita a intervalos de tiempo, también está la posibilidad de ejecutar funciones que respondan a eventos de dispositivos de E/S o intervalos de inactividad de un programa.

La manera de utilizar los bucles de ejecución es la siguiente:

  • Primero se crea la variable GMainloop
    • GMainLoop *bucle;

  • Luego el bucle
    • bucle = g_main_loop_new(NULL, FALSE);

  • Finalmente se ejecuta el bucle
    • g_main_loop_run(bucle);

  • Para detener el bucle se utiliza
    • g_main_loop_quit (bucle);

Una vez que se utilice g_main_loop_run, el control de la aplicación pasa a manos de Glib. Para poder hacer uso de las alarmas existen dos opciones, la primera cuando se cumple cierto intervalos de tiempo, g_timeout_add, y la otra cuando la aplicación no esta haciendo nada, g_idle_add.

Alarmas

Para utilizar un bucle conjuntamente con una alarma se realizan los pasos anteriormente explicados, pero se usa la siguiente función:

guint g_timeout_add (guint interval, GSourceFunc function, gpointer data);

Veamos los parámetros de esta función:

  • guint interval: Es el intervalo de tiempo en que se llama a la función, debe de estar en milisegundos.

  • GSourceFunc function: Función a llamar.

  • gpointer data: Dato que se pasa a la función como parámetro.

  • Retorno: El id del evento origen.

Tiempos de inactividad.

Lo mismo ocurre para los tiempos de inactividad, es decir aquellos en los que la aplicación no hace nada. Para poder usarlos se utiliza la siguiente función:

guint g_idle_add (GSourceFunc function, gpointer data);

Veamos los parámetros de esta función:

  • GSourceFunc function: Función a llamar.

  • gpointer data: Dato que se pasa a la función como parámetro.

  • Retorno: El id del evento origen.

Un factor a considerar es que en ambos casos cuando se llama a la función externa, la prioridad de esta es asignada de modo por omisión, G_PRIORITY_DEFAULT. Como ya se había comentado, la prioridad de la función a llamar es asignada automáticamente; así pues, la función puede ser retrasada por otros procesos de mayor prioridad.

En ambas funciones, la llamada a la función externa tiene que se de un tipo determinado, es decir, GSourceFunc representa un puntero a una función de la siguiente forma:

gboolean funcion_a_llamar (gpointer data);

Así la función es llamada en varias ocasiones hasta que devuelve FALSE y sale del bucle.

Ejemplo 6-6.1. Veamos un ejemplo de la forma de utilizar la función g_timeout_add.

   1 #include <glib.h>
   2 
   3 gboolean funcion_a_llamar(gpointer data);
   4 
   5 int
   6 main (int argc, char *argv[])
   7 {
   8         GMainLoop *bucle;
   9         guint b;
  10         guint i = 5000;
  11         
  12         bucle = g_main_loop_new(NULL, FALSE);
  13         b = g_timeout_add(i, funcion_a_llamar, bucle);
  14         g_main_loop_run(bucle);
  15         g_print("Ya salimos del bucle.\n");
  16         return 0;
  17 }
  18 
  19 gboolean funcion_a_llamar(gpointer data)
  20 {
  21         gint i=0;
  22         do{
  23                 g_print("Uso de g_timeout_add: %d\n", i);
  24                 i++;
  25         }while(i<3);
  26         g_print("Ahora vamos a finalizar el bucle.\n");
  27         g_main_loop_quit(data);
  28         
  29         return FALSE;
  30 }

Tratamiento de ficheros y canales de Entrada/Salida

Para el tratamiento de archivos y canales de entrada/salida en general, GLib provee un tipo de dato llamado GIOChannel, que permite encapsular archivos, sockets y tuberías. Esta abstracción no sólo posibilita el acceso uniforme a todos estos recursos, sino que también asegura portabilidad e integración al bucle de eventos.

Actualmente sólo hay soporte completo para UNIX, aunque el tratamiento con archivos puede hacerse independientemente de la plataforma.

Obtención de un GIOChannel.

Para crear un GIOChannel, primero se debe crear el descriptor de archivo de UNIX por los métodos convencionales (open, socket, etc.) y luego utilizar g_io_channel_unix_new o bien g_io_channel_new_file para acceder a archivos directamente.

GIOChannel *g_io_channel_unix_new(int fd);

Esta función devuelve el canal creado a partir del descriptor. Dicho parámetro se puede recuperar después utilizando la función g_io_channel_unix_get_fd.

GIOChannel *g_io_channel_new_file(const gchar *nombre_archivo,
                                  const gchar *modo, GError **error);

Devuelve un nuevo canal para acceder al archivo nombre_archivo. El modo tiene la misma forma que el parámetro modo de la función de C estándar fopen. Esto es:

  • r para lectura.

  • r+ para lectura y escritura.

  • w para escritura (Si el archivo no existe, lo crea; en caso contrario lo trunca).

  • w+ para lectura y escritura (con el archivo hace lo mismo que el parámetro w

  • a para escritura al final del archivo (lo crea si no existe).

  • a+ para lectura y escritura al final del archivo (lo crea si no existe).

El parámetro error debe apuntar a una estructura tipo GError previamente creada y es aquí donde se informa de cualquier problema al abrir el archivo. En este último caso la función retorna NULL.

Generalidades en el trabajo con GIOChannels.

Una vez obtenido el GIOChannel, en general, todas las llamadas a funciones para realizar alguna operación sobre el canal (lectura, escritura, etc.) tomarán como primer parámetro al propio canal y como último un puntero, error, a una estructura GError, donde se informará de cualquier inconveniente ocurrido al intentar la operación. Además, la mayoría de las funciones retornan un código del tipo GIOStatus que puede tomar los siguientes valores:

  • GIO_STATUS_ERROR: ocurrió un error (los detalles del mismo se encuentran en el parámetro error).

  • G_IO_STATUS_NORMAL: la operación se realizó con éxito.

  • G_IO_STATUS_EOF: se alcanzó el fin de archivo.

  • G_IO_STATUS_AGAIN: el recurso solicitado no está disponible (p.e., se intentó leer datos, pero no estaban en ese momento).

En caso de que el código de retorno sea G_IO_STATUS_ERROR, en la estructura GError se almacena otro código, indicando con mayor detalle la naturaleza del problema. Este nuevo código es de tipo GIOChannelError y puede tomar los siguientes valores:

  • G_IO_CHANNEL_ERROR_FBIG: el archivo es muy grande.

  • G_IO_CHANNEL_ERROR_INVAL: argumento inválido.

  • G_IO_CHANNEL_ERROR_IO: error general de entrada/salida.

  • G_IO_CHANNEL_ISDIR: el archivo es un directorio, pero se intentó acceder a él como un archivo regular.

  • G_IO_CHANNEL_ERROR_NOSPC: No hay más espacio disponible para escritura.

  • G_IO_CHANNEL_ERROR_NXIO: No existe tal dispositivo o dirección.

  • G_IO_CHANNEL_ERROR_OVERFLOW: el valor proporcionado es mayor de lo que permite su tipo.

  • G_IO_CHANNEL_ERROR_PIPE: algunos de los extremos de la tubería lo desconectó.

  • G_IO_CHANNEL_ERROR_FAILED: otros errores.

Los primeros valores tienen su correspondencia directa con los valores de la variable errno estándar de C. El último se reserva para otra clase de errores no identificados.

Los canales tienen un tiempo de vida controlado por conteo de referencias. Inicialmente, los canales se crean con una cuenta de 1 y, cuando la misma cae a 0, se destruyen. Para obtener una referencia se utiliza la función g_io_channel_ref y para liberar una referencia g_io_channel_unref. Cabe aclarar que, aunque el objeto GIOChannel se destruya, no necesariamente se cierra el canal. En concreto, si el objeto fue creado a partir de un descriptor de UNIX, el programador es responsable de cerrarlo (a menos que se utilice la función g_io_channel_set_close_on_unref.

Las funciones mencionadas tienen la siguiente forma:

void g_io_channel_ref(GIOChannel *canal);

void g_io_channel_unref(GIOChannel *canal);

Para cerrar de un canal se utiliza g_io_channel_shutdown:

GIOStatus g_io_channel_shutdown(GIOChannel *canal, gboolean descarga,
                                GError **error);

Aquí, el parámetro descarga controla si se escriben los datos que pueda haber en el buffer interno del canal antes de cerrarlo (para forzar una descarga en cualquier momento se puede utilizar g_io_channel_flush.

Operaciones básicas.

GLib provee una serie de funciones para acceder a un canal abierto de diversas maneras: por caracteres individuales, por bloques de cantidad de bytes fijos y por líneas.

Si el tipo de canal lo permite, la función g_io_channel_seek_position sirve para establecer la siguiente posición donde se leerá o escribirá. Normalmente sólo los archivos permiten posicionamiento aleatorio.

GIOStatus g_io_channel_seek_position(GIOChannel *canal, glong posicion,
                                     GSeekType tipo_posicion, GError **error);

La semántica de posicion (que es un número con signo) queda determinada por el tipo_posicion, que puede tomar los siguientes valores:

  • G_SEEK_CUR: la nueva posición es relativa a la actual.

  • G_SEEK_SET: la nueva posición se toma a partir del principio del archivo.

  • G_SEEK_END: la nueva posición es absoluta respecto al fin de archivo.

Para efectuar lecturas en un canal se utilizan algunas de las siguientes funciones:

GIOStatus g_io_channel_read_chars(GIOChannel *canal, gchar *buffer,
                                  gsize tamaño_buffer, gsize *bytes_leidos,
                                  GError **error);

Lee una cantidad máxima (tamaño_buffer) de bytes del canal como caracteres y los almacena en buffer. Éste debe estar previamente reservado (p.e. con g_malloc). bytes_leidos contiene la cantidad de bytes efectivamente leídos (puede ser menor que la solicitada).

Es importante notar que si el canal está configurado para trabajar con caracteres Unicode, un carácter puede ocupar más de un byte y, si el buffer no es lo suficientemente grande, puede que no sea posible almacenar ningún carácter. En estas condiciones bytes_leidos es cero, pero no hay indicación de error.

GIOStatus g_io_channel_read_line(GIOChannel *canal, gchar **puntero_cadena,
                                 gsize *bytes_leidos, gsize *terminador,
                                 GError **error);

Lee una línea completa del canal y almacena el puntero a la misma en puntero_cadena. Una vez finalizada la operación con la línea, es responsabilidad del programador liberar la memoria con g_free. En bytes_leidos se devuelve la cantidad total de bytes leídos (incluyendo caracteres de fin de línea) y en terminador, sólo la cantidad perteneciente a la línea en sí. Tanto bytes_leidos como terminador pueden ser nulos si la información no es de interés.

GIOStatus g_io_channel_read_line_string(GIOChannel *canal, GString *cadena,
                                        gsize *terminador, GError **error);

Similar a g_io_channel_read_line, sólo que, para devolver el resultado, utiliza una estructura GString. terminador puede ser nulo.

GIOStatus g_io_channel_read_to_end(GIOChannel *canal, gchar **puntero_cadena,
                                   gsize *bytes_leidos, GError **error);

Lee todos los bytes disponibles en el canal hasta encontrar el fin de archivo. En puntero_cadena se devuelve un puntero al espacio de memoria asignado a tal fin que luego debe ser liberado con g_free.

Para la escritura se utiliza la contra parte de g_io_channel_read_chars:

GIOStatus g_io_channel_write_chars(GIOChannel *canal, gchar *buffer,
                                   gsize cantidad_bytes, gsize *bytes_escritos,
                                   GError **error);

Como su propio nombre indica, esta función escribe en el canal cantidad_bytes del buffer. Puede que no lleguen a escribirse todos los bytes que se solicitaron, por lo que la función devuelve la cantidad de bytes efectivamente escritos. Si cantidad_bytes es -1 se considera al buffer como terminado en nulo.

Se aplican las mismas restricciones en cuanto a codificación del canal que para g_io_channel_read_chars.

Integración de canales al bucle de eventos.

Uno de los aspectos más interesantes de los GIOChannels es que es posible integrarlos fácilmente al bucle de eventos y así, por ejemplo, hacer que la aplicación reaccione ante la llegada de datos.

Los posibles tipos de eventos que puede generar un canal están dados por el tipo GIOCondition que toma los siguientes valores:

  • G_IO_IN: hay datos disponibles en el canal.

  • G_IO_OUT: se pueden escribir datos al canal sin que éste bloquee.

  • G_IO_PRI: hay datos urgentes para leer.

  • G_IO_ERR: condición de error.

  • G_IO_HUP: desconexión; normalmente, se aplica a sockets y tuberías. Alguno de los extremos rompió la conexión.

  • G_IO_NVAL: solicitud no válida (p.e. el descriptor UNIX no está abierto).

La forma básica de integrar un objeto generador de eventos al bucle es obtener una estructura GSource. En el caso de los canales, la función para ello es g_io_create_watch:

GSource *g_io_create_watch(GIOChannel *canal, GIOCondition condición);

Esta función devuelve un GSource que luego se registra en algún bucle de eventos con g_source_attach. Sin embargo, por conveniencia, GLib provee un par adicional de funciones para integrar directamente un canal al bucle principal: g_io_watch y g_io_add_watch_full.

guint g_io_add_watch(GIOChannel *canal, GIOCondition condición,
                     GIOFunc función, gpointer datos_usuario);

Esta función, que devuelve el identificador de evento, registra el canal para que sea controlado según las condiciones especificadas. En caso de que se cumpla alguna de las condiciones, se llama a la función con los datos_usuario adicionales.

La función especificada en el registro debe tener la siguiente forma:

gboolean (* GIOFunc)(GIOChannel *canal, GIOCondition condición,
                     gpointer datos_usuario);

La condición que la función recibe es aquella que causó el evento en primer lugar. datos_usario es lo que se especificó en el momento del registro.

La otra función, g_io_add_watch_full permite además especificar la prioridad del canal y una función de notificación cuando el canal se desconecta del bucle de eventos.

Configuración avanzada de canales.

Codificación

Los GIOChannels son capaces de codificar o decodificar los caracteres que se escriben o leen del mismo. La codificación que se utiliza internamente es UTF-8, por lo que, eventualmente, se puede transformar de UTF-8 a alguna otra codificación para escribir a un archivo, o viceversa.

Si se necesita que un canal no efectúe transformación alguna a los datos (por ejemplo, para tratamiento de datos binarios) se debe optar por la codificación nula (NULL).

Las funciones para manipular la codificación son g_io_channel_set_encoding y g_io_channel_get_encoding.

GIOStatus g_io_channel_set_encoding(GIOChannel *canal,
                                    const gchar *codificación, GError **error);

G_CONST_RETURN gchar *g_io_channel_get_encoding(GIOChannel *canal);

Por omisión, la codificación externa utilizada para archivos es UTF-8. Para cambiarla se deben tener en cuenta las siguientes condiciones:

  • El canal acaba de ser creado.
  • El canal es para escritura solamente.
  • El canal es sobre un archivo y el puntero de lectura y escritura acaba de ser colocado con g_io_channel_seek_position.

  • La codificación actual es UTF-8 o nula.
  • Se ha llamado a una función de lectura y ha devuelto G_IO_STATUS_EOF o, en el caso de g_io_channel_read_to_end, G_IO_STATUS_NORMAL.

Terminador de línea.

En UNIX, el fin de línea se indica con un carácter de linefeed (código ASCII 10), pero esto puede no ser cierto para otras plataformas (p.e. Win32).

Para que las funciones de lectura y escritura por líneas den los resultados esperados para cada plataforma, en caso de ser necesario el acceso a archivos escritos en otra plataforma, se debe configurar el terminador de línea en el canal. Las funciones para ello son:

void g_io_channel_set_line_term(GIOChannel *canal, const gchar *terminador,
                                gint longitud);

G_CONST_RETURN gchar *g_io_channel_get_line_term(GIOChannel *canal,
                                                 gint *longitud);

Configuración de buffer interno.

La API de GIOChannel permite modificar alguno de los parámetros de funcionamiento interno del canal. Es posible modificar el tamaño del buffer interno del mismo para optimizar el rendimiento de lectura y escritura en casos especiales. Para ello se utilizan las funciones g_io_channel_get_buffer_size y g_io_channel_set_buffer_size.

gsize g_io_channel_get_buffer_size(GIOChannel *canal);

void g_io_channel_set_buffer_size(GIOChannel *canal, gsize tamaño);

Si en g_io_channel_set_buffer_size se especifica un tamaño 0, GLib elegirá un tamaño adecuado al canal.

Se ha visto antes que, utilizando el bucle de eventos, se puede supervisar un canal y producir llamadas a funciones ante determinados eventos. También es posible verificar manualmente si un canal tiene datos listos para la lectura o si se pueden escribir datos en él. Para ello se utiliza la función g_io_channel_get_buffer_condition.

GIOCondition g_io_channel_get_buffer_condition(GIOChannel *canal);

Esta función puede devolver solamente G_IO_IN y G_IO_OUT y, por supuesto, su significado es exactamente el mismo que se explicó antes.

Por último, con las funciones [#g_io_channel_set_flags g_io_channel_set_flags] y [#g_io_channel_get_flags g_io_channel_get_flags], es posible saber y, en parte, modificar el comportamiento que tiene un canal.

GIOFlags g_io_channel_get_flags(GIOChannel *canal);

GIOStatus g_io_channel_set_flags(GIOChannel *canal, GIOFlags banderas,
                                 GError **error);

Los valores que puede tomar el parámetro banderas son G_IO_FLAG_APPEND y/o G_IO_FLAG_NONBLOCK (los demás valores son de sólo lectura). El valor obtenido en g_io_channel_get_flags es una composición de:

  • G_IO_FLAG_APPEND: canal en modo de agregado.

  • G_IO_FLAG_NONBLOCK: canal en modo no bloqueante; esto es, si se intenta, p.e., leer y no hay datos disponibles, se devuelve el control inmediatamente con un estado de G_IO_STATUS_AGAIN.

  • G_IO_FLAG_IS_READABLE: el canal se puede leer.

  • G_IO_FLAG_IS_WRITEABLE: se puede escribir en el canal.

  • G_IO_FLAG_IS_SEEKABLE: se puede mover el puntero en el canal mediante la utilización de g_io_channel_seek_position.

Manejo de memoria dinámica

Sobre el tratamiento de memoria, GLib dispone de una serie de instrucciones que sustituyen a las ya conocidas por todos malloc, free, etc. y, siguiendo con el modo de llamar a las funciones en GLib, las funciones que sustituyen a las ya mencionadas son g_malloc y g_free.

Reserva de memoria.

La función g_malloc posibilita la reserva de una zona de memoria, con un número de bytes que le pasemos como parámetro. Además, también existe una función similar llamada g_malloc0 que, no sólo reserva una zona de memoria, sino que, además, llena esa zona de memoria con ceros, lo cual nos puede beneficiar si se necesita un zona de memoria totalmente limpia.

gpointer g_malloc (gulong numero_de_bytes );

gpointer g_malloc0 (gulong numero_de_bytes );

Existe otro conjunto de funciones que nos permiten reservar memoria de una forma parecida a cómo se hace en los lenguajes orientados a objetos. Esto se realiza mediante las siguientes macros definidas en GLib /gmem.h:

   1         /* Convenience memory allocators
   2         */
   3         #define g_new(struct_type, n_structs)           \
   4                 ((struct_type *) g_malloc (((gsize) sizeof (struct_type)) * ((gsize) (n_structs))))
   5         #define g_new0(struct_type, n_structs)          \
   6                 ((struct_type *) g_malloc0 (((gsize) sizeof (struct_type)) * ((gsize) (n_structs))))
   7         #define g_renew(struct_type, mem, n_structs)    \
   8                 ((struct_type *) g_realloc ((mem), ((gsize) sizeof (struct_type)) * ((gsize) (n_structs))))
   9 

Como se puede apreciar, no son más que macros basadas en g_malloc, g_malloc0 y g_realloc. La forma de funcionamiento de g_new y g_new0 es mediante el nombre de un tipo de datos y un número de elementos de ese tipo de datos, de forma que se puede hacer:

   1         GString *str = g_new (GString,1);
   2         GString *arr_str = g_new (GString, 5);

En estas dos líneas de código, se asigna memoria para un elemento de tipo GString, que queda almacenado en la variable str, y para un array de cinco elementos de tipo GString, que queda almacenado en la variable arr_str.

g_new0 funciona de la misma forma que g_new, con la única diferencia de que inicializa a 0 toda la memoria asignada. En cuanto a g_renew, ésta funciona de la misma forma que g_realloc, es decir, reasigna la memoria asignada anteriormente.

Liberación de memoria.

Cuando se hace una reserva de memoria con g_malloc y, en un momento dado, el uso de esa memoria no tiene sentido, es el momento de liberar esa memoria. Y el sustituto de free es g_free que, básicamente, funciona igual que la anteriormente mencionada.

void g_free (gpointer memoria_reservada );

Se presenta un ejemplo sencillo sobre el uso de g_malloc () y de g_free ().

Ejemplo 6-8.1. Uso de g_malloc () y g_free ().

   1 /* ejemplo del uso de g_malloc y g_free */
   2 #include <glib.h>
   3 int main (int argc, char *argv[])
   4 {
   5         gchar *text;
   6         gint len = strlen ("Hola a todos") + 1;
   7         text = g_malloc (len);
   8         g_snprintf (text, len, "Hola a todos");
   9         g_print ("%s\n", text);
  10         g_free (text);
  11         return 0;
  12 }

Realojamiento de memoria.

En determinadas ocasiones, sobre todo cuando se utilizan estructuras de datos dinámicas, es necesario ajustar el tamaño de una zona de memoria (ya sea para hacerla más grande o más pequeña). Para eso, GLib ofrece la función g_realloc, que recibe un puntero a memoria que apunta a una región que es la que será acomodada al nuevo tamaño y devuelve el puntero a la nueva zona de memoria. El anterior puntero es liberado y no se debería utilizar más:

gpointer g_realloc (gpointer memoria_reservada , gulong numero_de_bytes );

Perfil de uso de memoria.

Un perfil de uso de memoria es la información sobre el uso de memoria que hace nuestra aplicación. Para obtener un perfil de nuestro programa se hace uso de la siguiente función:

void g_mem_profile (void );

Para que se obtenga la información sobre el uso de memoria de nuestra aplicación se debe de inicializar una tabla de perfiles de memoria, esto se logra mediante la variable extern GMemVTable *glib_mem_profiler_table; la cual se debe de usar conjuntamente con g_mem_set_vtable() la cual debe ser llamada antes que cualquier otra función de glib.

Veamos un ejemplo sobre su uso:

Ejemplo 6-8.2. Uso de g_mem_profile ()

   1 /* ejemplo del uso de g_mem_profile */
   2 #include <glib.h>
   3 int main (int argc, char *argv[])
   4 {
   5         gchar *text;
   6         gint len = strlen ("Hola a todos") + 1;
   7         g_mem_set_vtable (glib_mem_profiler_table);
   8         text = g_malloc (len);
   9         g_snprintf (text, len, "Hola a todos");
  10         g_print ("%s\n", text);
  11         g_free (text);
  12         g_mem_profile ();
  13         return 0;
  14 }

El resultado es el siguiente:

Figura 6-8.1. Salida de g_mem_profile ().

mem_profile.png

Primero se invoca g_mem_set_vtable (). para la preparación del ambiente y asi obtener un perfil de la memoria empleada, y antes de finalizar pedimos un resumen con la función g_mem_profile ().

Estructuras de datos: listas enlazadas, pilas y colas

Listas enlazadas.

Introducción

La lista enlazada es un TDA que nos permite almacenar datos de una forma organizada, al igual que los vectores pero, a diferencia de estos, esta estructura es dinámica, por lo que no tenemos que saber "a priori" los elementos que puede contener.

En una lista enlazada, cada elemento apunta al siguiente excepto el último que no tiene sucesor y el valor del enlace es null. Por ello los elementos son registros que contienen el dato a almacenar y un enlace al siguiente elemento. Los elementos de una lista, suelen recibir también el nombre de nodos de la lista.

            struct lista {
            gint dato;                                           (1)
            lista *siguiente;                                    (2)
            };

(1)

  • Representa el dato a almacenar. Puede ser de cualquier tipo; en este ejemplo se trata de una lista de enteros.

(2)

  • Es un puntero al siguiente elemento de la lista; con este puntero enlazamos con el sucesor, de forma que podamos construir la lista.

Figura 6-9.1. Esquema de un nodo y una lista enlazada.

lista.png

Para que esta estructura sea un TDA lista enlazada, debe tener unos operadores asociados que permitan la manipulación de los datos que contiene. Los operadores básicos de una lista enlazada son:

  • Insertar: inserta un nodo con dato x en la lista, pudiendo realizarse esta inserción al principio o final de la lista o bien en orden.
  • Eliminar: elimina un nodo de la lista, puede ser según la posición o por el dato.
  • Buscar: busca un elemento en la lista.
  • Localizar: obtiene la posición del nodo en la lista.
  • Vaciar: borra todos los elementos de la lista

Después de esta breve introducción, que sólo pretende servir como recordatorio, pasaremos a ver cómo es la estructura GSList que, junto con el conjunto de funciones que la acompañan, forman el TDA lista enlazada en GLib.

GSList

La definición de la estructura GSList o, lo que es lo mismo, un nodo de la lista, está definido de la siguiente manera:

            struct GSList {
              gpointer data;                                     (3)
              GSList *next;                                      (4)
            };

(3)

  • Representa el dato a almacenar. Se utiliza un puntero genérico por lo que puede almacenar un puntero a cualquier tipo de dato o bien almacenar un entero utilizando las macros de conversión de tipos.

(4)

  • Se trata de un puntero al siguiente elemento de la lista.

Las macros de conversión disponibles son las siguientes:

  • GINT_TO_POINTER ()

  • GPOINTER_TO_INT ()

  • GUINT_TO_POINTER ()

  • GPOINTER_TO_UINT ()

Más adelante, en esta misma sección, se verán ejemplos del uso de estas macros.

Las funciones que acompañan a la estructura GSList y que implementan los operadores básicos de las listas enlazadas, son las siguientes:

Tabla 6-9.1. Operadores de inserción en listas enlazadas.

Operador

Funciones asociadas a GSList.

Insertar al principio.

GSList* g_slist_prepend (GSList *list, gpointer data)

Insertar al final.

GSList* g_slist_append (GSList *list, gpointer data)

Insertar en la posición indicada.

GSList* g_slist_insert (GSList *list, gpointer data, gint position)

Insertar en orden.

GSList* g_slist_insert_sorted (GSList *list, gpointer data, GCompareFunc func)

Las funciones de inserción al principio de la lista, g_slist_prepend, y al final, g_slist_append, son sencillas de usar. Sólo hay que pasarles como parámetros la lista donde queremos añadir el dato así como el dato a insertar y la función devuelve una lista con el nuevo dato insertado.

La función g_slist_insert inserta el dato en la posición indicada. Su uso también es sencillo como puede verse en el siguiente ejemplo.

Ejemplo 6-9.1. Insertar un nuevo dato en una posición determinada.

   1     /* obtiene el numero de nodos de la lista */
   2     length = g_slist_length (list);
   3 
   4     g_print ("\nEscribe el nº de indice donde se insertara el dato (el indice maximo es %d): ", length);
   5     scanf ("%d", &index);
   6 
   7     /* inserta el valor en la posicion indicada */
   8 
   9     if (index < length) {
  10       list = g_slist_insert (list, GINT_TO_POINTER (value), index);
  11       print_list (list);
  12         }

En este ejemplo se utiliza la función g_slist_length para obtener el número de nodos que contiene la lista. A esta función hay que pasarle como parámetro la lista de la que se desea obtener el número de nodos y devuelve como resultado el número de nodos de ésta.

guint * g_slist_length ( GSList * list );

La función g_slist_insert_sorted inserta los elementos a la lista de forma ordenada. Esta función utiliza el parámetro GCompareFunc para insertar el dato en la posición correcta.

GCompareFunc es una función que se utiliza para comparar dos valores y saber así cual de ellos hay que insertar primero. En los dos ejemplos que hay a continuación, se puede observar una función de tipo GCompareFunc y su uso para insertar datos en una lista en orden creciente.

Ejemplo 6-9.2. Parámetro GCompareFunc para insertar en orden creciente.

   1 gint compare_value1 (gconstpointer a, gconstpointer b) {
   2   gint *value1 = (gint *) a;
   3   gint *value2 = (gint *) b;
   4 
   5   return value1 > value2;
   6 }

Ejemplo 6-9.3. Insertar elementos en orden creciente.

   1   gint values[] = {8, 14, 5, 12, 1, 27, 3, 13};
   2   gint i;
   3   /* insertando valores en orden creciente */
   4   for (i = 0; i < 8; i++) {
   5     list =  g_slist_insert_sorted (list, GINT_TO_POINTER (values[i]),
   6                                    compare_value1);
   7   }

Tabla 6-9.2. Operadores de eliminación en listas enlazadas.

Operador

Funciones asociadas a GSList.

Eliminar un nodo.

GSList* g_slist_remove (GSList *list, gconstpointer data)

Eliminar nodos según un patrón.

GSList* g_slist_remove_all (GSList *list, gconstpointer data)

Las dos funciones expuestas para la eliminación de nodos, si bien tienen una definición prácticamente idéntica, el resultado obtenido es distinto. En el caso de g_slist_remove, se eliminará el nodo que contenga el valor data. Si hay varios nodos con el mismo valor, sólo se eliminará el primero. Si ningún nodo contiene ese valor, no se realiza ningún cambio en el GSList. En el caso de g_slist_remove_all, se eliminan todos los nodos de la lista que contengan el valor data y nos devuelve la nueva lista resultante de la eliminación de los nodos.

Ejemplo 6-9.4. Elimina un elemento de la lista.

   1   if (list2 != NULL) {
   2     g_print ("\nEl dato %d sera eliminado de la lista.\n", list2->data);
   3 
   4     /* eliminando un elemento de la lista */
   5     g_slist_remove (list, list2->data);
   6    }

Tabla 6-9.3. Operadores de búsqueda en listas enlazadas.

Operador

Funciones asociadas a GSList.

Buscar un nodo según un valor.

GSList* g_slist_find (GSList *list, gconstpointer data)

Buscar un nodo según un criterio.

GSList* g_slist_find_custom (GSList *list, gconstpointer data, GCompareFunc func)

Localizar el índice de un nodo.

GSList* g_slist_index (GSList *list, gconstpointer data)

Localizar la posición de un nodo.

GSList* g_slist_position (GSList *list, GSList *llink)

Obtener el último nodo.

GSList* g_slist_last (GSList *list)

Obtener el siguiente nodo.

g_slist_next (slist)

Obtener un nodo por su posición.

GSList* g_slist_nth (GSList *list, guint n)

Obtener el dato de un nodo según su posición.

gpointer g_slist_nth_data (GSList *list, guint n)

Todas estas funciones, a excepción de g_slist_nth_data, devuelven un nodo de la lista o NULL si el elemento no existe. La función g_slist_nth_data devuelve el valor del elemento según la posición que se le pasa como argumento en el parámetro n o NULL si la posición que se le pasa está más allá del final de la lista.

La función g_slist_next, es una macro que nos devuelve el siguiente nodo. Esta macro la podemos utilizar para recorrer la lista.

Ejemplo 6-9.5. Función que imprime una lista.

   1 void print_list (GSList *list) {
   2 
   3   gint i = 0;
   4 
   5   while (list != NULL) {
   6     g_print ("Node %d content: %d.\n", i, list->data);
   7 
   8     /* apunta al siguiente nodo de la lista */
   9     list = g_slist_next (list);
  10     i++;
  11   }
  12 }

Tabla 6-9.4. Operador para vaciar la lista.

Operador

Funciones asociadas a GSList.

Vacía la lista y libera la memoria usada.

void g_slist_free (GSList *list)

La función g_slist_free libera la memoria de la lista que se le pasa como parámetro.

Con estas funciones, quedan definidos los operadores básicos del TDA lista enlazada. GSList trae otras funciones además de los operadores básicos. Para más información sobre estas, está disponible el manual de referencia de GLib.

Listas doblemente enlazadas.

Introducción.

El TDA lista doblemente enlazada, al igual que la lista enlazada, es un TDA dinámico lineal pero, a diferencia de este, cada nodo de la lista doblemente enlazada contiene dos punteros, de forma que uno apunta al siguiente nodo y el otro al predecesor. Esta característica, permite que se pueda recorrer la lista en ambos sentidos, cosa que no es posible en las listas simples.

La declaración del tipo lista doblemente enlazada de enteros es la siguiente:

            struct lista_doble {
            gint dato;                                           (5)
            lista_doble *siguiente;                              (6)
            lista_doble *anterior;                               (7)
            };

(5)

  • Representa el dato a almacenar, que puede ser de cualquier tipo. En este ejemplo se trataría de una lista de enteros.

(6)

  • Se trata de un puntero al siguiente elemento de la lista. Con este puntero se enlaza con el sucesor, de forma que podamos construir la lista.

(7)

  • Es un puntero al elemento anterior de la lista. Este puntero enlaza con el elemento predecesor de la lista y permite recorrerla en sentido inverso.

Sobre este TDA se definen los mismos operadores básicos que en las listas simples.

GList

La definición de la estructura GList, que es un nodo de la lista doblemente enlazada, está definido de la siguiente manera:

            struct GList
            {
            gpointer data;                                       (8)
            GList *next;                                         (9)
            GList *prev;                                         (10)
            };

(8)

  • Representa el dato que se va a almacenar. Se utiliza un puntero genérico por lo que puede almacenar un puntero a cualquier tipo de dato o bien almacenar un entero utilizando las macros de conversión de tipos.

(9)

  • Se trata de un puntero al siguiente elemento de la lista.

(10)

  • Se trata de un puntero al elemento anterior de la lista.

Las funciones que acompañan a la estructura GList, que implementan los operadores básicos de las listas enlazadas, son las siguientes:

Tabla 6-9.5. Operadores de inserción en listas doblemente enlazadas.

Operador

Funciones asociadas a GList

Insertar al principio.

GList* g_list_prepend (GList *list, gpointer data)

Insertar al final.

GList* g_list_append (GList *list, gpointer data)

Insertar en la posición indicada.

GList* g_list_insert (GList *list, gpointer data, gint position)

Insertar en orden.

GList* g_list_insert_sorted (GList *list, gpointer data, GCompareFunc func)

Como puede observarse en la definición de las funciones, su uso es el mismo que en las listas simples, al igual que las macros de conversión, por lo que todo lo explicado en esa sección es válido en el caso de las listas doblemente enlazadas.

Ejemplo 6-9.6. Insertar un nuevo dato en una posición determinada.

   1     /* obtiene el numero de nodos de la lista */
   2     length = g_list_length (list);
   3 
   4     g_print ("\nEscribe el numero de indice donde se insertara el dato (el indice maximo es %d): ", length);
   5     scanf ("%d", &index);
   6 
   7     /* inserta el valor en la posicion indicada */
   8 
   9     if (index < length) {
  10       list = g_list_insert (list, GINT_TO_POINTER (value), index);
  11       print_list (list);
  12         }

Ejemplo 6-9.7. Insertar elementos en orden creciente.

   1   gint values[] = {8, 14, 5, 12, 1, 27, 3, 13};
   2   gint i;
   3 
   4   for (i = 0; i < 8; i++) {
   5     list =  g_list_insert_sorted (list, GINT_TO_POINTER (values[i]),
   6                                    compare_value1);
   7   }

Tabla 6-9.6. Operadores de eliminación en listas doblemente enlazadas.

Operador

Funciones asociadas a GList

Eliminar un nodo.

GList* g_list_remove (GList *list, gconstpointer data)

Eliminar nodos según un patrón.

GList* g_list_remove_all (GList *list, gconstpointer data)

Ejemplo 6-9.8. Eliminar un elemento de la lista.

   1   if (list2 != NULL) {
   2     g_print ("\nEl dato %d sera eliminado de la lista.\n", list2->data);
   3 
   4     /* eliminando un elemento de la lista */
   5     g_list_remove (list, list2->data);
   6     print_list (list);
   7    }

Tabla 6-9.7. Operadores de búsqueda en listas doblemente enlazadas.

Operador

Funciones asociadas a GList.

Buscar un nodo según un valor.

GList* g_list_find (GList *list, gconstpointer data)

Buscar un nodo según un criterio.

GList* g_list_find_custom (GList *list, gconstpointer data, GCompareFunc func)

Localizar el índice de un nodo.

GList* g_list_index (GList *list, gconstpointer data)

Localizar la posición de un nodo.

GList* g_list_position (GList *list, GSList *llink)

Obtener el último nodo.

GList* g_list_last (GList *list)

Obtener el siguiente nodo.

g_list_next (list)

Obtener un nodo por su posición.

GList* g_list_nth (GList *list, guint n)

Obtener el dato de un nodo según su posición.

gpointer g_list_nth_data (GList *list, guint n)

Ejemplo 6-9.9. Busca un valor dado en la lista.

   1   g_print ("\nEntra un valor entero a buscar: ");
   2   scanf ("%d", &value);
   3   g_print ("\n");
   4 
   5   /* buscando un elemento en la lista */
   6   list2 = g_list_find (list, GINT_TO_POINTER (value));
   7 
   8   if (list2 != NULL) {
   9     index = g_list_index (list, list2->data);
  10     g_print ("\nEl valor %d esta en el nodo %d.\n", list2->data, index);

Tabla 6-9.8. Operador para vaciar la lista

Operador

Funciones asociadas a GList

Vacía la lista y libera la memoria usada.

void g_list_free (GList *list)

Con estas funciones, quedan definidos los operadores básicos del TDA lista enlazada. Al igual que GSList, GList trae otras funciones además de los operadores básicos. Para más información sobre estas, está disponible el manual de referencia de GLib.

GQueue: pilas y colas.

Otra de las novedades que incorpora esta versión de GLib es la estructura GQueue que, junto con las funciones que la acompañan, nos proporciona la posibilidad de implementar los TDA cola y pila estándar.

GQueue utiliza estructuras GList para almacenar los elementos. La declaración de la estructura de datos es la siguiente.

            struct GQueue {
               GList  *head;                                     (11)
               GList  *tail;                                     (12)
               guint length;                                     (13)
               };

(11)

  • Es un puntero al primer elemento de la cola.

(12)

  • Es un puntero al último elemento de la cola.

(13)

  • Esta variable almacena el número de elementos de la cola.

Aunque para referirnos al TDA GQueue utilizamos el término cola, con esta misma estructura también tenemos la posibilidad de implementar el TDA pila, gracias a las funciones que lo acompañan.

Colas

Una cola es una estructura de datos donde el primer elemento en entrar es el primero en salir, también denominadas estructuras FIFO (First In, First Out).

Esta estructura de datos se puede definir como una lista enlazada con acceso FIFO a la que sólo se tiene acceso al final de la lista para meter elementos y al principio de esta para sacarlos.

Los operadores asociados a este TDA y las funciones que los implementan en GLib son:

Tabla 6-9.9. Operadores asociados al TDA Cola.

Operador

Funciones asociadas a GQueue.

Iniciar cola.

GQueue* g_queue_new (void)

Cola vacía.

gboolean g_queue_is_empty (GQueue* queue)

Consultar frente cola.

gpointer g_queue_peek_head (GQueue* queue)

Consultar final cola.

gpointer g_queue_peek_tail (GQueue* queue)

Meter

void g_queue_push_tail (GQueue* queue, gpointer data)

Sacar

gpointer g_queue_pop_head (GQueue* queue)

Vaciar cola.

void g_queue_free (GQueue* queue)

Iniciar cola.

El operador "Iniciar cola" es el encargado de crear una nueva cola y ponerla en estado de cola vacía.

Ejemplo 6-9.10. Creando una nueva cola.

   1    GQueue* cola;
   2    cola = g_queue_new ();

Cola vacía.

Este operador consulta si la cola está vacía. Es necesaria su utilización antes de realizar la operación de "sacar elementos" de la cola.

Ejemplo 6-9.11. Función que comprueba si una cola está vacía.

   1 gboolean cola_vacia (GQueue* cola) {
   2    return g_queue_is_empty (cola);
   3 }

Consultar el frente.

Esta operación consulta el contenido del frente de la cola sin sacarlo.

Ejemplo 6.9-12. Función que consulta el frente de la cola.

   1 gpointer consultar_frente (GQueue* cola) {
   2    return g_queue_peek_head (cola);
   3 }

Consultar el final.

Esta operación consulta el contenido del final de la cola sin sacarlo.

Ejemplo 6-9.13. Función que consulta el final de la cola.

   1 gpointer consultar_final (GQueue* cola) {
   2    return g_queue_peek_tail (cola);
   3 }

Meter

Este operador introduce elementos al final de la cola.

Ejemplo 6-9.14. Introducir un nuevo elemento en la cola.

   1 GQueue* meter_cola (GQueue* cola, gpointer dato) {
   2    g_queue_push_tail (cola, dato);
   3 
   4    return cola;
   5 }

Sacar

El operador "sacar" elimina elementos del frente de la cola.

Ejemplo 6-9.15. Saca un elemento de la cola.

   1 gpointer sacar_cola (GQueue* cola) {
   2    gpointer dato;
   3 
   4    dato = g_queue_pop_head (cola);
   5 
   6    return dato;
   7 }

Vaciar cola.

Elimina el contenido de una cola inicializándola a una cola vacía.

Ejemplo 6-9.16. Vacía la cola.

   1   g_queue_free (cola);

Pilas

Una pila, es una estructura de datos en la que el último elemento en entrar es el primero en salir, por lo que también se denominan estructuras LIFO (Last In, First Out).

En esta estructura sólo se tiene acceso a la cabeza o cima de la pila.

Los operadores asociados a este TDA y las funciones que los implementan en GLib son:

Tabla 6-9.10. Operadores asociados al TDA Pila.

Operador

Funciones asociadas a GQueue.

Iniciar pila.

GQueue* g_queue_new (void)

Pila vacía.

gboolean g_queue_is_empty (GQueue* queue)

Consultar pila.

gpointer g_queue_peek_head (GQueue* queue)

Meter.

void g_queue_push_head (GQueue* queue, gpointer data)

Sacar.

gpointer g_queue_pop_head (GQueue* queue)

Vaciar pila.

void g_queue_free (GQueue* queue)

Iniciar pila.

El operador "iniciar pila" es el encargado de crear una nueva pila y inicializarla al estado de pila vacía.

Ejemplo 6-9.17. Creando una nueva pila.

   1    GQueue* pila;
   2    pila = g_queue_new ();

Pila vacía.

Este operador consulta si la pila está vacía. Es necesaria su utilización antes de realizar la operación de sacar elementos de la pila.

Ejemplo 6-9.18. Función que comprueba si una pila está vacía.

   1 gboolean pila_vacia (GQueue* pila) {
   2    return g_queue_is_empty (pila);
   3 }

Consultar pila.

Esta operación, consulta el contenido de la cima de la pila sin sacarlo.

Ejemplo 6-9.19. Función que consulta la cima de la pila.

   1 gpointer consultar_pila (GQueue* pila) {
   2    return g_queue_peek_head (pila);
   3 }

Meter

El operador "meter", introduce elementos en la cima de la pila.

Ejemplo 6-9.20. Introducir un nuevo elemento en la pila.

   1 GQueue* meter_pila (GQueue* pila, gpointer dato) {
   2    g_queue_push_head (pila, dato);
   3 
   4    return pila;
   5 }

Sacar

El operador sacar, saca elementos de la cima de la pila.

Ejemplo 6-9.21. Saca un elemento de la pila.

   1 gpointer sacar_pila (GQueue* pila) {
   2    gpointer dato;
   3 
   4    dato = g_queue_pop_head (pila);
   5 
   6    return dato;
   7 }

Vaciar pila.

Elimina el contenido de una pila inicializándola a una pila vacía.

Ejemplo 6-9.22. Vacía la pila

   1   g_queue_free (pila);

Estructuras de datos avanzadas

Tablas de dispersión.

Qué es una tabla de dispersión.

Las tablas de dispersión, más conocidas como tablas hash, son unas de las estructuras de datos más frecuentemente usadas. Para tener una idea inicial, las tablas de dispersión posibilitan tener una estructura que relaciona una clave con un valor, como un diccionario. Internamente, las tablas de dispersión son un array. Cada una de las posiciones del array puede contener ninguna, una o varias entradas del diccionario. Normalmente contendrá una como máximo, lo que permite un acceso rápido a los elementos, evitando realizar una búsqueda en la mayoría de los casos. Para saber en qué posición del array se debe buscar o insertar una clave, se utiliza una función de dispersión. Una función de dispersión relaciona a cada clave con un valor entero. Dos claves iguales deben tener el mismo valor de dispersión, también llamado hash value, pero dos claves distintas pueden tener el mismo valor de dispersión, lo cual provocaría una colisión.

El valor de dispersión es un entero sin signo entre 0 y el máximo entero de la plataforma, por lo que la tabla de dispersión usa el resto de dividir el valor de dispersión entre el tamaño del array para encontrar la posición. Cuando dos claves tienen que ser almacenadas en la misma posición de la tabla se produce una colisión. Esto puede ser debido a que la función de dispersión no distribuye las claves lo suficiente, o a que hay más claves en la tabla hash que el tamaño del array. En el segundo caso, GLib se encargará de redimensionar el array de forma automática.

Para aclarar los conceptos, se examinará el siguiente ejemplo a lo largo de todo el capítulo con el fin de facilitar la comprensión del mismo. Póngase que se desea tener una relación de la información de los activistas de GNOME. Para ello se usará una tabla de dispersión usando como clave el apodo que usa cada desarrollador dentro del proyecto. Y como valor una relación de sus datos personales. Esta relación de datos personales vendría dada por la siguiente estructura.

   1                 struct
   2                 DatosDesarrollador {
   3                    gchar *apodo;
   4                    gchar *nombre;
   5                    gchar *proyecto;
   6                    gchar *telefono;
   7                 };

Ahora, si se hace un recuento de datos, se obtiene una curiosa información. Por ejemplo, si tenemos en cuenta que cada apodo tiene como mucho diez caracteres y que el alfabeto tiene veintiseis letras, el resultado es que tendremos 26^10 posibles llaves, lo que supera con creces el número de claves que vamos a utilizar. Con ésto se quiere hacer hincapié en que el uso de esta estructura es útil cuando el número de llaves libres excede con mucho a las llaves que van a ser ocupadas.

Cómo se crea una tabla de dispersión.

Para crear una tabla de dispersión se tiene que indicar la función de dispersión y la función que se utilizará para comparar dos claves. Estas dos funciones se deben pasar como parámetros a la función g_hash_table_new

GHashTable* g_hash_table_new (GHashFunc funcion_de_dispersion,
                              GEqualFunc funcion_de_comparación );

GLib tiene implementadas una serie de funciones de dispersión y comparación. De este modo, el desarrollador no ha de reinventar la rueda. Las funciones de dispersión trabajan siempre de la misma manera. A este tipo de funciones se les pasa un valor y devuelven la clave. En cuanto a las funciones de comparación, se les pasa como parámetros dos valores y devuelve TRUE si son los mismos valores y FALSE si no son iguales.

Estas funciones son implementadas basándose en el siguiente esquema. Si se diera el caso de que las funciones existentes en GLib no satisfacen sus necesidades, lo único que tendrá que hacer será implementar un par de funciones que siguieran estos esquemas.

guint (*GHashFunc) (gconstpointer clave );

gboolean (*GEqualFunc) (gconstpointer clave_A , gconstpointer clave_B );

Una vez se tiene presente este esquema, se puede comprender mejor el funcionamiento de las funciones que se describirán a continuación. La primera pareja de funciones son g_str_hash y g_str_equal, que son de dispersión y de comparación respectivamente. Con la función g_str_hash podremos convertir una clave en forma de cadena en un valor de dispersión. Lo cual quiere decir que el desarrollador podrá usar, con estas funciones, una clave en forma de cadena para la tabla hash.

Otras funciones de carácter similar son g_int_hash, g_int_equal, g_direct_hash o g_direct_equal, que posibilitan usar como claves para la tabla de dispersión números enteros y punteros genéricos respectivamente.

GHashTable* g_hash_table_new_full (GHashFunc       funcion_de_dispersion,
                                   GEqualFunc      funcion_de_comparacion,
                                   GDestroyNotify  key_destroy_func,
                                   GDestroyNotify  value_destroy_func)

La función g_hash_table_new_full es muy parecida a g_hash_table_new, pero a diferencia de esta última, introduce la utilización de dos nuevas funciones, la primera de ellas para de liberar la memoria asignada para la clave y la segunda es usada para liberar la memoria asignada para el valor, ambas funciones son utilizadas al eliminarse una entrada de la tabla de dispersión, y en ambos casos pueden ser reemplazadas por NULL, si no se deseara proveer de dichas funciones.

Ahora, siguiendo con el ejemplo que se comenzó al principio del capitulo, se pasará a ver cómo se creará la tabla de dispersión que contendrá la información de los desarrolladores de GNOME. Recuérdese que se iba a usar, como clave, el apodo del desarrollador. Así que, como función de dispersión y de comparación, se usaran g_str_hash y g_str_equal, respectivamente. Ya que el apodo es una cadena y estas funciones satisfacen perfectamente el fin que se persigue.

Ejemplo 6-10.1. Utilización de tabla de dispersión.

   1 #include <glib.h>
   2 

Cómo manipular un tabla de dispersión.

Ahora se pasará a describir las funciones de manipulación de este TAD. Para añadir y eliminar elementos de una tabla se dispone de estas dos funciones: g_hash_table_insert y g_hash_table_remove. La primera insertará un valor en la tabla hash y le indicará la clave con la que después podrá ser referenciado. En cuanto a la segunda, borrará el valor que corresponda a la clave que se le pase como parámetro a la función.

void g_hash_table_insert (GHashTable tabla_hash, gpointer clave,
                          gpointer valor);

gboolean g_hash_table_remove (GHashTable tabla_hash, gpointer clave );

En caso que se desee modificar el valor asociado a una clave, hay dos opciones a seguir en forma de funciones: g_hash_table_insert (vista anteriormente) o g_hash_table_replace. La única diferencia entre ambas es qué pasa cuando la clave ya existe. En el caso de la primera, se conservará la clave antigua mientras que, en el caso de la segunda, se usará la nueva clave. Obsérvese que dos claves se consideran la misma si la función de comparación devuelve TRUE, aunque sean diferentes objetos.

void g_hash_table_replace (GHashTable tabla_hash, gpointer clave,
                           gpointer valor );

Búsqueda de información dentro de la tabla.

Llegados a este punto, en el que se ha explicado como insertar, borrar y modificar elementos dentro de una tabla de este tipo, parece obvio que el siguiente paso es cómo conseguir encontrar información dentro de una tabla de dispersión. Para ello se dispone de la función g_hash_table_lookup. Esta función tiene un funcionamiento muy simple al igual que las funciones anteriormente explicadas. Lo único que necesita es que se le pase como parámetro la tabla de dispersión y la clave que desea buscar en la misma y la función devolverá un puntero al valor asociado a la clave en caso de existir o el valor NULL en caso de no existir.

gpointer g_hash_table_lookup (GHashTable tabla_hash, gpointer clave );

También se dispone de otra función para realizar las búsquedas de datos dentro de una tabla de dispersión. Esta función es más compleja en su uso pero provee de un sistema de búsqueda más eficiente que el anterior que ayudará a suplir las necesidades más complejas de un desarrollador.

   1 gboolean g_hash_table_lookup_extended (GHashTable tabla_hash,
   2                                        gconstpointer clave_a_buscar,
   3                                        gpointer* clave_original,
   4                                        gpointer* valor );

La función anterior devolverá TRUE si encuentra el valor buscado, o FALSE en caso contrario. En el supuesto de encontrar el valor, el puntero clave_original apuntará a la clave con la que fue almacenado en la tabla, y el puntero valor apuntará al valor almacenado. Téngase en cuenta que la clave usada para la realizar la búsqueda, clave_a_buscar, no tiene por qué ser la misma que la encontrada, clave_original, aunque ambas serán iguales según la función de comparación que hayamos indicado al crear la tabla de dispersión.

Manipulación avanzada de tablas de dispersión.

TODO

Arboles binarios balanceados

Los árboles binarios balanceados son estructuras de datos que almacenan pares clave - dato, de manera ordenada según las claves, y posibilitan el acceso rápido a los datos, dada una clave particular, y recorrer todos los elementos en orden.

Son apropiados para grandes cantidades de información y tienen menor overhead que una tabla de dispersión, aunque el tiempo de acceso es de mayor complejidad computacional.

Si bien internamente la forma de almacenamiento tiene estructura de árbol, esto no se exterioriza en la API, haciendo que el manejo de los datos resulte transparente. Se denominan balanceados porque, cada vez que se modifican, las ramas se rebalancean de tal forma que la altura del árbol sea la mínima posible, acortando de esta manera el tiempo promedio de acceso a los datos.

Creación de un árbol binario.

Para crear un árbol binario balanceado es necesaria una función de comparación que pueda ordenar un conjunto de claves. Para ello se adopta la convención utilizada por strcmp. Esto es, dados dos valores, la función devuelve 0 si son iguales, un valor negativo si el primer parámetro es anterior al segundo, y un valor positivo si el primero es posterior al segundo. La utilización de esta función permite flexibilidad en cuanto a claves a utilizar y en como se ordenan. El prototipo es el siguiente:

gint (*GCompareFunc)(gconstpointer a, gconstpointer b);

o bien, para el caso en que sea necesario proveer algún dato adicional a la función:

gint (*GCompareDataFunc)(gconstpointer a, gconstpointer b, gpointer user_data);

Una vez definida dicha función el árbol se crea con alguna de las siguientes formas:

GTree *g_tree_new(GCompareFunc key_compare_func);

GTree *g_tree_new_with_data(GCompareDataFunc key_compare_func,
                            gpointer key_compare_data);

GTree *g_tree_new_full(GCompareDataFunc key_compare_func,
                       gpointer key_compare_data,
                       GDestroyNotify key_destroy_func,
                       GDestroyNotify value_destroy_func);

donde key_compare_func es la función previamente mencionada, key_compare_data es un dato arbitrario a enviar a la función de comparación y key_destroy_func y value_destroy_func funciones de retrollamada cuando alguna clave o dato se eliminan del árbol.

En caso de no utilizar la última función, es tarea del usuario liberar la memoria utilizada por claves y/o datos cuando se destruye el árbol o cuando se elimina algún dato del mismo utilizando g_tree_remove.

Para destruir un árbol se utiliza la función g_tree_destroy.

void g_tree_destroy(GTree *tree);

Agregar y eliminar elementos a un árbol binario.

Para insertar un nuevo elemento en el árbol se utiliza g_tree_insert o g_tree_replace. La diferencia entre ambas vale sólo en el caso de que en el árbol ya exista una clave igual a la que se intenta agregar y sólo cuando, en la creación del árbol, se especificó una función de destrucción de claves. Para g_tree_insert la clave que se pasó como parámetro se destruye, llamando a key_destroy_func y la del árbol permanece inalterada. Para el caso de g_tree_replace, la clave ya existente se destruye y se reemplaza por la nueva. En ambos casos, de existir el elemento, el dato anterior se destruye llamando a value_destroy_func, siempre que se haya especificado en la creación del árbol.

void g_tree_insert(GTree *tree, gpointer key, gpointer value);

void g_tree_replace(GTree *tree, gpointer key, gpointer value);

key es la clave con la cual se recuperará el dato luego y value el dato en sí. Para eliminar un elemento del árbol se pueden utilizar g_tree_remove o g_tree_steal. La diferencia entre ambas es que, si se invoca g_tree_steal no se destruyen la clave y el valor aunque se hayan especificado funciones para tal fin en la creación del árbol.

void g_tree_remove(GTree *tree, gconstpointer key);

void g_tree_steal(GTree *tree, gconstpointer key);

Búsqueda y recorrida en un árbol binario.

Existen dos funciones para buscar en un árbol binario, similares a las utilizadas en tablas de hash: g_tree_lookup y g_tree_lookup_extended.

gpointer g_tree_lookup(GTree *tree, gconstpointer key);

En este caso, se busca un elemento del árbol cuya clave sea igual (en los términos de la función especificada al momento de la creación) a key. Devuelve el dato del elemento encontrado. Si la búsqueda fue infructuosa devuelve NULL.

gboolean g_tree_lookup_extended(GTree *tree, gconstpointer lookup_key,
                                gpointer *orig_key, gpointer *value);

Esta función no sólo busca el elemento cuya clave coincida con la especificada, sino que además devuelve la clave y dato del elemento encontrado en los parámetros de salida orig_key y value. A diferencia del caso anterior, la función devuelve un booleano indicando si la búsqueda fue exitosa o no.

La versión extendida de la búsqueda es particularmente útil cuando se quiere eliminar un elemento del árbol sin destruirlo, utilizando la función g_tree_steal (p.e. para ser agregado a otro árbol).

Por último para recorrer todos los elementos de un árbol se utiliza g_tree_foreach. Los elementos son tomados en orden y pasados como parámetro a la función de iteración especificada.

void g_tree_foreach(GTree *tree, GTraverseFunc func, gpointer user_data);

Es importante decir que durante el recorrido, no se puede modificar el árbol (agregar y/o eliminar elementos). Si se quieren eliminar algunos elementos, se deben agregar las claves a una lista (o cualquier estructura auxiliar) y finalizada la iteración proceder a eliminar uno por uno los elementos seleccionados. La función de iteración debe tener la siguiente forma:

gboolean (*GTraverseFunc)(gpointer key, gpointer value, gpointer data);

Si dicha función devuelve TRUE, la iteración se interrumpe. data es el valor que se especificó como user_data en la función g_tree_foreach.

GNode : Arboles de orden n.

Para representar datos de manera jerárquica, GLib ofrece el tipo de dato GNode que permite representar árboles de cualquier orden.

Al igual que con las listas y a diferencia de árboles binarios balanceados o tablas hash, los GNode son elementos independientes: sólo hay conexiones entre ellos, pero nada en la estructura dice, por ejemplo, cuál es la raíz del árbol. NULL es el árbol vacío. La estructura GNode tiene la siguiente forma:

   1       struct GNode {
   2          gpointer  data;
   3          GNode    *next;
   4          GNode    *prev;
   5          GNode    *parent;
   6          GNode    *children;
   7       };

data contiene el dato en sí del nodo. parent apunta al nodo padre: éste es NULL para el nodo raíz. children apunta al primer hijo del nodo, accediéndose a los demás hijos mediante los campos next y prev.

Agregar nodos a un árbol.

GLib tiene una serie de funciones para agregar nodos a un árbol. Se puede crear el nodo aislado primero e insertarlo en una posición dada, pero también hay macros definidas que crean el nodo y lo insertan directamente. No hay diferencia en el resultado final, simplemente una línea menos de código.

Para crear un nodo se utiliza g_node_new:

GNode *g_node_new(gpointer data);

Una vez obtenido el GNode, se inserta a un árbol especificando el nodo padre (parent) ya existente y la posición relativa entre los hijos de dicho padre. Hay cinco funciones diferentes en función de la posición y las condiciones en que se quiera insertar. En todos los casos el valor devuelto es el mismo nodo node insertado.

GNode *g_node_insert(GNode *parent, gint position, GNode *node);

GNode *g_node_insert_before(GNode *parent, GNode *sibling, GNode *node);

GNode *g_node_insert_after(GNode *parent, GNode *sibling, GNode *node);

GNode *g_node_append(GNode *parent, GNode *node);

GNode *g_node_prepend(GNode *parent, GNode *node);

sibling es un nodo hijo de parent que se toma como referencia para insertar el nuevo nodo antes o después de él mismo. En ambos casos, g_node_insert_after y g_node_insert_before, si sibling es NULL, el nuevo nodo se inserta al final de la lista de hijos.

Con g_node_n_children se obtiene la cantidad de hijos de un nodo y, a partir de este dato, se puede especificar una posición relativa dentro de la lista de hijos position. Las formas g_node_append y g_node_prepend agregan el nuevo nodo al final o al principio de la lista de hijos.

Cuatro de estas cinco funciones tienen su equivalente de inserción de nodo directa a partir de el dato. Como se dijo antes, la diferencia es que en estos casos se especifica el dato y la creación del nodo es transparente al usuario aunque, de ser requerido, se retorna en el valor de la función. Estas cuatro nuevas formas son macros que utilizan las funciones anteriores. En todos los casos, data es el dato que contendrá el nuevo nodo.

GNode *g_node_insert_data(GNode *parent, gint position, gpointer data);

GNode *g_node_insert_data_before(GNode *parent, GNode *sibling, gpointer data);

GNode *g_node_append_data(GNode *parent, gpointer data);

GNode *g_node_prepend_data(GNode *parent, gpointer data);

Eliminar Vs. desvincular.

Al igual que con las listas, hay dos formas de quitar un dato de un árbol: g_tree_unlink y g_tree_destroy.

void g_node_unlink(GNode *node);

void g_node_destroy(GNode *node);

g_node_unlink quita el nodo especificado del árbol, resultando en un nuevo árbol que lo tiene como raíz. g_node_destroy no sólo elimina el nodo del árbol, sino que, además, libera toda la memoria utilizada por el nodo y por sus hijos. GLib, sin embargo, no tiene forma de liberar la memoria de los datos que contenían los nodos: eso es responsabilidad del usuario.

Información sobre los nodos.

G_NODE_IS_LEAF devuelve TRUE si el nodo es un terminal u hoja del árbol. En otras palabras si el campo children es NULL. Análogamente, G_NODE_IS_ROOT devuelve TRUE si el nodo especificado es la raíz del árbol, o bien, si el campo parent es NULL.

g_node_depth devuelve la profundidad de un nodo (cuantos niveles hay hasta subir a la raíz); g_node_n_children la cantidad de nodos hijos inmediatos y g_node_max_height, la cantidad máxima de niveles inferiores (es decir, iterando hacia los hijos). g_node_depth y g_node_max_height miden alturas. Un árbol vacío tiene altura 0, un único nodo 1 y así sucesivamente.

guint g_node_n_nodes(GNode *node, GTraverseFlags flags);

g_node_n_nodes recorre el árbol desde el nodo especificado y cuenta los nodos. El parámetro flags indica qué nodos debe contar y puede tener como valores:

G_TRAVERSE_LEAFS: Sólo contar los nodos terminales G_TRAVERSE_NON_LEAFS: Sólo contar los nodos intermedios G_TRAVERSE_ALL: Contar todos los nodos

gboolean g_node_is_ancestor(GNode *node, GNode *descendant);

g_node_is_ancestor devolverá TRUE si node es un ancestro (padre, padre del padre, etc.) de descendant.

g_node_get_root devuelve la raíz del árbol que contiene al nodo especificado.

gint g_node_child_index(GNode *node, gpointer data);

gint g_node_child_position(GNode *node, GNode *child);

g_node_child_index y g_node_child_position son funciones similares que devuelven la posición de un hijo en la lista de hijos de un nodo (la primera busca el dato en lugar del nodo en sí). En caso de que el nodo no sea hijo del padre especificado, ambas funciones devolverán -1.

Para acceder a los hijos de un nodo dado se pueden utilizar los campos de la estructura o funciones que provee GLib: g_node_first_child, g_node_last_child y g_node_nth_child.

En forma similar, se puede recorrer la lista de hijos usando los campos prev y next, o mediante g_node_first_sibling, g_node_prev_sibling, g_node_next_sibling y g_node_last_sibling.

Buscar en el árbol y recorrerlo.

Para buscar un nodo determinado, GLib tiene dos funciones:

GNode *g_node_find(GNode *node, GTraverseType order, GTraverseFlags flags,
                   gpointer data);

GNode *g_node_find_child(GNode *node, GTraverseFlags flags, gpointer data);

g_node_find y g_node_find_child buscan a partir de node el nodo del árbol que contenga el data especificado. g_node_find busca en todo el árbol, mientras que g_node_find_child se limita a los hijos inmediatos del nodo.

En ambos casos flags especifica que clase de nodos se buscarán, y en g_node_find, order indica el orden de búsqueda. Este último puede tener uno de estos cuatro valores:

  • G_IN_ORDER: para cada nodo, visitar el primer hijo, luego el nodo mismo y por último el resto de los hijos.

  • G_PRE_ORDER: para cada nodo, visitar primero el nodo y luego los hijos.

  • G_POST_ORDER: para cada nodo, visitar primero los hijos y luego el nodo.

  • G_LEVEL_ORDER: visitar los nodos por niveles, desde la raíz (es decir la raíz, luego los hijos de la raíz, luego los hijos de los hijos de la raíz, etc.)

Al igual que con otros tipos de dato, es posible recorrer el árbol con dos funciones de GLib, que son análogas a las de búsqueda:

void g_node_traverse(GNode *root, GTraverseType order,
                     GTraverseFlags flags, gint max_depth,
                     GNodeTraverseFunc func, gpointer data);

void g_node_children_foreach(GNode *node, GTraverseFlags flags,
                             GNodeForeachFunc func, gpointer data);

max_depth especifica la profundidad máxima de iteración. Si este valor es -1 se recorre todo el árbol; si es 1 sólo root; y así sucesivamente. order indica cómo recorrer los nodos y flags qué clase de nodos visitar. Notar que al igual que g_node_find_child, g_node_children_foreach se limita a los hijos directos del nodo.

La forma de las funciones de iteración es la siguiente:

gboolean (*GNodeTraverseFunc)(GNode *node, gpointer data);

gboolean (*GNodeForeachFunc)(GNode *node, gpointer data);

Al igual que con las funciones de iteración de los árboles binarios, en caso de necesitar interrumpir la iteración, la función debe devolver TRUE.

Caches

Los cachés son algo que, tarde o temprano, cualquier programador acaba implementando, pues permiten el almacenamiento de datos costosos de conseguir (un fichero a través de la red, un informe generado a partir de datos en una base de datos, etc) para su posterior consulta, de forma que dichos datos sean obtenidos una única vez, y leídos muchas.

Como librería de propósito general que es, GLib incluye, no un sistema de caché superfuncional, si no una infraestructura básica para que se puedan desarrollar cachés de todo tipo. Para ello, GLib incluye GCache, que, básicamente, permite la compartición de estructuras complejas de datos. Esto se usa, principalmente, para el ahorro de recursos en las aplicaciones, de forma que, si se tienen estructuras de datos complejas usadas en distintas partes de la aplicación, se pueda compartir una sola copia de dicha estructura compleja de datos entre todas esas partes de la aplicación.

Creación del caché.

GCache funciona de forma muy parecida a las tablas de claves (GHashTable), es decir, mediante el uso de claves para identificar cada objeto, y valores asociados con esas claves.

Pero el primer paso, como siempre, es crear el caché en cuestión. Para ello se usa la función g_cache_new, que, a primera vista, parece un tanto compleja:

GCache g_cache_new (GCacheNewFunc value_new_func,
                    GCacheDestroyFunc value_destroy_func,
                    GCacheDupFunc key_dup_func,
                    GCacheDestroyFunc key_destroy_func,
                    GHashFunc hash_key_func,
                    GHashFunc hash_value_func,
                    GEqualFunc key_equal_func);

Todos los parámetros que recibe esta función son parámetros a funciones y, para cada uno de ellos, se debe especificar la función que realizará cada una de las tareas, que son:

  • Creación de nuevas entradas en el caché (value_new_func). Esta función será llamada por g_cache_insert (ver más adelante) cuando se solicite la creación de un elemento cuya clave no existe en el caché.

  • Destrucción de entradas en el caché (value_destroy_func). En esta función, que es llamada cuando se destruyen entradas en el caché, se debe liberar toda la memoria alojada para la entrada del caché que va a ser destruida. Normalmente, esto significa liberar toda la memoria alojada en value_new_func.

  • Duplicación de claves. Puesto que las claves son asignadas fuera del interfaz de GCache (es decir, por la aplicación que haga uso de GCache), es tarea de la aplicación la creación o duplicación de claves. Así, esta función es llamada cada vez que GCache necesita duplicar una clave.

  • Destrucción de claves. Al igual que en el caso de las entradas, las claves también deben ser destruidas, liberando toda la memoria que estén ocupando.
  • Gestión de la tabla de claves incorporada en GCache. Al igual que cuando se usan los GHashTable, con GCache tambien es necesario especificar las funciones de manejo de la tabla de claves asociada, y para ello se usan los tres últimos parámetros de g_cache_new.

Como se puede observar, GCache está pensado para ser extendido, no para ser usado directamente. Esto es a lo que se hacía referencia en la introducción, de que es una infraestructura para la implementación de cachés. GCache implementa la lógica del caché, mientras que el manejo de los datos de ese caché se dejan de la mano de la aplicación, lo cual se obtiene mediante la implementación de todas estas funciones explicadas en este punto.

Es necesario en este punto hacer un pequeño alto y ver con detalle cómo debe ser la implementación de estas funciones.

Gestión de los datos del caché.

Una vez que el manejo de los datos del caché están claramente definidos en las funciones comentadas, llega el momento de usar realmente el caché. Para ello, se dispone de un amplio conjunto de funciones.

La primera operación que viene a la mente es la inserción y obtención de datos de ese caché. Esto se realiza mediante el uso de una única función: g_cache_insert.

gpointer g_cache_insert (GCache *cache, gpointer key);

Esta función busca el objeto con la clave key especificada. Si lo encuentra ya disponible en el caché, devuelve el valor asociado a esa clave. Si no lo encuentra, lo crea, para lo cual llama a la función especificada en el parámetro value_new_func en la llamada a g_cache_new. Así mismo, si el objeto no existe, la clave es duplicada, para lo cual, como no podía ser de otra forma, se llama a la función especificada en el parámetro key_dup_func de g_cache_new.

Podría ser una buena idea usar cadenas como claves para especificar determinados recursos y, mediante esos identificadores, crear la estructura compleja de datos asociada en la función de creación de entradas del caché. De esta forma, por ejemplo, se podría implementar fácilmente un caché de ficheros obtenidos a través de diferentes protocolos: las claves usadas serían los identificadores únicos de cada fichero (URLs), mientras que la entrada en el caché contendría el contenido mismo del fichero.

Pero, puesto que no siempre se accede al contenido del caché conociendo de antemano las claves, GLib incluye funciones que permiten acceder secuencialmente a todos los contenidos del caché. Esto se puede realizar de dos formas, y, por tanto, con dos funciones distintas:

void g_cache_key_foreach(GCache *cache, GHFunc func, gpointer user_data);

void g_cache_value_foreach(GCache *cache, GHFunc func, gpointer user_data);

Ambas funciones operan de la misma forma, que es llamar retroactivamente a la función especificada en el parámetro func por cada una de las entradas en el caché. La diferencia entre ellas es que g_cache_key_foreach opera sobre las claves y g_cache_value_foreach opera sobre los valores.

En cualquiera de los dos casos, cuando se realice una llamada a una de estas funciones, se deberá definir una función con la siguiente forma:

   1           void foreach_func (gpointer key, gpointer value, gpointer user_data);

En key se recibe la clave de cada una de las entradas (una cada vez), mientras que value recibe el valor de dicha entrada. Por su parte, user_data es el mismo dato que el que se especifica en la llamada a g_cache_*_foreach, y que permite pasar datos de contexto a la función de retrollamada.

Otra operación importante que se puede realizar en un caché es la eliminación de entradas. Para ello, se usa la función g_cache_remove.

void g_cache_remove(GCache *cache, gconstpointer value);

Esta función marca la entrada identificada por value para ser borrada. No la borra inmediatamente, sino que decrementa un contador interno asociado a cada entrada, que especifica el número de referencias que dicha entrada tiene. Dichas referencias se asignan a 1 cuando se crea la entrada, y se incrementan cada vez que g_cache_insert recibe una petición de creación de esa misma entrada. Así, cuando dicho contador llega a 0, significa que no hay ninguna referencia a la entrada del caché, y por tanto, puede ser eliminada. Cuando la entrada es eliminada, se realizan llamadas a las funciones value_destroy_func para la destrucción de la entrada y key_destroy_func para la destrucción de la clave, tal y como se especificó en la explicación de la función g_cache_new.

Destrucción del caché.

Una vez que no sea necesario por más tiempo el caché, hay que destruirlo, como con cualquier otra estructura de GLib, de forma que se libere toda la memoria ocupada. Esto se realiza con la función g_cache_destroy, cuyo único parámetro es el caché que se quiere destruir.

void g_cache_destroy(GCache *cache);

GLib Avanzado

Hilos en Glib.

Los hilos permiten que un programa realice varias tareas simultáneamente. Tradicionalmente, los sistemas operativos tipo UNIX/linux han usado los procesos para este propósito. No obstante, éstos son mucho más pesados (requieren más recursos) que los hilos.

Sin embargo, cada sistema operativo ha implementado de una forma diferente el concepto de hilo y, como resultado, las implementaciones no son compatibles, aunque sean muy parecidas. Ésto implica que si, se desea usar hilos en el programa será necesario proveer de una implementación diferente para cada sistemas operativo tipo UNIX.

GLib solventa este problema ocultando las implementaciones especificas de cada plataforma y proveyendo de un API que puede ser accedido de la misma forma desde cualquier sistema operativo. No obstante, internamente se usará las llamadas nativas disponibles en la plataforma donde se encuentre la aplicación. Es decir, GLib abstrae la implementación de los hilos.

Antes de entrar en materia, es necesario hacer una observación: todos los hilos que un programa crea, comparten el mismo espacio de direcciones; esto es, se podrá acceder a las variables globales de nuestro programa (para lectura y escritura) desde cualquier hilo. Inicialización y Creación de Hilos.

Antes de poder usar cualquier función de manejo de hilos, hay que preparar el sistema para ejecutar estas operaciones, es decir, es necesario inicializar el sistema de hilos, nada más sencillo.

Void g_thread_init (GThreadFunctions *vtable );

En la mayoría de las ocasiones bastará con que el parámetro pasado al sistema de hilos sea nulo, de esta forma GLib se encargará de buscar los parámetros más apropiados.

Es importante no llamar a esta función más de una vez pues, si esto sucediese, la ejecución de nuestro programa sería abortada. Para evitar esto la forma más sencilla es hacer:

   1           if (!g_thread_supported ())
   2           g_thread_init (NULL);

Después, el sistema estará listo para realizar cualquier operación que requiera el uso de hilos. Para empezar, se creará un hilo:

   1 GThread *hilo:
   2 
   3           hilo = g_thread_create (funcion_hilo, NULL, FALSE, NULL);

A partir de este punto, el procedimiento funcion_hilo se ejecutará en un hilo diferente, el programa continuará su ejecución normal y, en paralelo a él, se ejecutará esta función. Sincronización entre hilos.

Como ya se ha dicho, las variables globales del programa van a poder ser accedidas desde cualquier hilo. Esto es una gran ventaja, pues compartir información entre todas las hilos será muy sencillo; no obstante, todo tiene su parte negativa: habrá que tener cuidado a la hora de acceder a estos valores, porque puede que un hilo intente modificar alguna variable cuando otro hilo está leyendo esa misma variable, en cuyo caso, el resultado sería impredecible. Es necesario que los hilos se sincronicen para acceder a estas variables.

Existen varios métodos los sincronización disponibles:

  • Cerrojos (Mutex)
  • Cerrojos estáticos (Static Mutex)
  • Variables Condición

Cerrojos: son llaves mutuamente exclusivas, que permiten conocer si se puede o no acceder a una determinada variable. Si una variable está protegida por un cerrojo, se podrá consultar el estado de la llave para conocer si está siendo accedida por otro hilo en ese instante. Si no lo estuviese, será posible obtener la llave, hacer las operaciones precisas sobre esa variable, y después abrir la llave de nuevo para que otro hilo pueda acceder a la variable.

          g_mutex_new
          g_mutex_lock
          g_mutex_trylock
          g_mutex_unlock
          g_mutex_free

Los cerrojos estáticos (Static Mutex) son iguales que las anteriores, pero no necesitan ser creadas en tiempo de ejecución; son creadas cuando el programa es compilado.

          g_static_mutex_init
          g_static_mutex_lock
          g_static_mutex_trylock
          g_static_mutex_unlock
          g_static_mutex_get_mutex
          g_static_mutex_free

Condiciones: son estructuras de datos que representan condiciones, Los hilos pueden ser bloqueados si encuentran una determinada condición como false. Si otro hilo cambia el estado de la condición, este envia una señal lo cual provoca que el hilo inicial despierte.

          g_cond_new
          g_cond_signal
          g_cond_broadcast
          g_cond_wait
          g_cond_timed_wait
          g_cond_free

UTF-8: las letras del mundo.

Antes de explicar el uso de las funciones que proporciona GLib para el uso de cadenas UTF-8, sería conveniente repasar una serie de conceptos que serán útiles para una mejor conmprensión.

Unicode: Una forma de representación (UCS).

ISO establece un estándar en el que define el conjunto de caracteres universal conocido como UCS. Este conjunto de caracteres asegura la compatibilidad con otros conjuntos de caracteres, de forma que se puedan usar distintas funciones para pasar de un determinado lenguaje a UCS y de este al lenguaje original sin perder contenido.

Gracias al Unicode se pueden representar la enorme mayoría de lenguajes conocidos, tales como latín, griego, cirílico, hebreo y así un largo etcétera. Unicode es un conjunto de caracteres de 16 bits, es decir, posee 65536 posibles combinaciones, representando cada una de estas combinaciones un carácter.

Un valor o carácter UCS o Unicode, indistintamente llamado de una forma u otro, englobará todos estos lenguajes, partiendo en otras codificaciones más pequeñas como BMP (Basic Multilingual Plane) o Plane 0, subconjunto de 16 bits, al igual que otros como UTF-16, de 16 bits, o UTF-8, de 8 bits, ambos utilizados posteriormente por la GLib tanto para comprobaciones de tipos de caracteres como para conversiones entre distintos subconjuntos Unicode, tales como UTF-8 y UTF-16. Qué es Unicode.

Al principio, se intentaron dar dos intentos de conseguir un conjunto de caracteres: el proyecto de estandarización ISO 10646 antes comentado y el proyecto Unicode, pero pronto se comprobó que no era bueno tener dos proyectos en marcha y los dos conjuntos de caracteres diferentes se unieron formando una única tabla de caracteres que fue la de Unicode con las extensiones dadas por la ISO 10646 pasando a ser llamado indistintamente Unicode o ISO 10646. Con el paso del tiempo ha habido modificaciones de Unicode adiciones de caracteres y clasificaciones en subconjuntos como pueden ser el ampliamente usado ASCII, UTF-8, UTF-16 y BMP entre otros.

Qué es UTF-8.

En GLib existe soporte para UTF-8 debido a sus compatibilidades con el resto de códigos, ya que su codificación engloba en sus primeros 7 bits el tan conocido ASCII.

La codificación UTF-8 está definida en el ISO 10646.

  • Los primeros 7 bits son utilizados para la codificación de ASCII, para la compatibilidad con éste por su uso extendido. De forma qu,e desde 0000 a 007F (128 caracteres), se usan para la codificación ASCII de 7 bits.
  • Los caracteres mayores a 007F son utilizados para el soporte a otros lenguajes.
  • Los bytes FE y FF no son utilizados para la codificación UTF-8.

Conversiones del juego de caracteres.

En determinadas ocasiones, los programas necesitan el uso de más de un juego de caracteres para un correcto tratamiento de los datos que maneja. Para que esto sea posible sin conflictos ni confusiones, GLib proporciona un útil conjunto de funciones para la conversión entre juegos de caracteres.

Nombres de archivos y UTF-8.

Con todo lo explicado anteriormente, es evidente que el juego de caracteres usado para representar nombres de archivos y el juego de caracteres UTF-8 que podamos usar en nuestros programas puede diferir bastante. Para solventar esto existen las funciones g_filename_to_utf8 y g_filename_from_utf8, que realizan la conversión de nombre de archivo a UTF-8 y de UTF-8 a nombre de archivo, respectivamente.

gchar * g_filename_to_utf8 (const gchar * cadena_a_convertir,
                            gssize longitud_de_cadena,
                            gsize * bytes_leidos,
                            gsize * bytes_escritos,
                            GError ** error);

gchar * g_filename_from_utf8 (const gchar * cadena_a_convertir,
                              gssize longitud_de_cadena,
                              gsize * bytes_leidos,
                              gsize * bytes_escritos,
                              GError ** error);

Como se puede observar, el uso de las dos funciones es idéntica porque devuelven la cadena convertida al nuevo juego de caracteres o NULL en caso de que ocurra un error. Los parámetros bytes_leidos, bytes_escritos y error son parámetros de salida, devolviendo respectivamente los bytes convertidos con éxito, los bytes que ocupa la cadena tras la transformación y el posible error que se pueda producir.

Configuración local de caracteres y UTF-8.

De forma parecida al tratamiento de nombres de archivo, las funciones g_locale_to_utf8 y g_locale_from_utf8 convierten cadenas en el juego de caracteres usado internamente por el sistema a cadenas en UTF-8 y viceversa.

gchar * g_locale_to_utf8 (const gchar * cadena_a_convertir,
                          gssize longitud_de_cadena,
                          gsize * bytes_leidos,
                          gsize * bytes_escritos,
                          GError ** error);

gchar * g_locale_from_utf8 (const gchar * cadena_a_convertir,
                            gssize longitud_de_cadena,
                            gsize * bytes_leidos,
                            gsize * bytes_escritos,
                            GError ** error);

El uso de las funciones es idéntico al descrito con g_filename_to_utf8 y g_filename_from_utf8.

Otras funciones relacionadas con conversión de cadenas.

Además de las funciones de conversión de cadenas, GLib provee además de un conjunto de funciones destinadas a servir de ayuda y apoyo al tratamiento de cadenas en UTF-8.

La función g_get_charset es útil para conocer el juego de caracteres que esta usando el sistema sobre el que se ejecuta el programa, pudiendo evitar transformaciones redundantes.

gboolean g_get_charset (G_CONST_RETURN gchar ** juego_de_caracteres );

La función devuelve TRUE si el juego de caracteres que usa el sistema es el UTF-8 y FALSE en cualquier otro caso. juego_de_caracteres es un parámetro de salida que devulve el juego de caracteres usado por el sistema en formato cadena.

La función g_utf8_validate sirve determinar si una cadena de caracteres está ya en formato UTF-8 o no.

gboolean g_utf8_validate (gchar * cadena_a_validar, gssize longitud,
                          const gchar ** final);

El parámetro cadena_a_validar es la cadena que se quiere validar. longitud es el número máximo de caracteres que queremos que valide, puede ser NULL si queremos validar la cadena entera; por último, en el último parámetro, la función devuelve un puntero al último carácter que ha podido validar. La función devuelve TRUE o FALSE dependiendo de si la cadena pasada cumple el formato UTF-8 o no.

Como hacer plugins.

Una de las cosas que pueden interesar a la hora de hacer una aplicación es crear un método por el cual se pueda aplicar extensiones. Ésto se conoce por el nombre de plugin. Para los desarrolladores habituales, el término "plugin" no debería resultar desconocido, ya que existen aplicaciones en GNOME, como por ejemplo gedit, gnumeric o algunos reproductores multimedia que aprovechan este método para extender la funcionalidad de la aplicación.

Para entender cómo funcionan los plugins, hay que tener claro el concepto de "símbolo". En una biblioteca, a cada función o variable se le da un nombre único (símbolo). Los símbolos que interesan ahora, son los que se exportan, que serán los que se utilicen para enlazarla. Por ejemplo, cuando en un código se llama a una función, lo que se hace es especificar el símbolo de la biblioteca y, en consecuencia, a la función que se desea. Ésto se hace automáticamente a la hora de programar. Pero también se puede hacer manualmente. O sea, se puede especificar una biblioteca en concreto (el plugin) y llamar a una función en concreto. De este modo se podría crear muchas bibliotecas (plugins), que además implementen una función común en todas ellas y se podría cargar manualmente esa función de cualquiera de los plugins. Para luego ejecutarla dentro de el programa. Ésto se verá mucho más claro más adelante, con el ejemplo de esta sección.

Para realizar este tipo de acciones se dispone de GModule. Éste es un sistema que permitirá la carga dinámica de símbolos. Además, siguiendo con una de las máximas de GLib, este sistema es portable. Lo que nos permitirá cargar símbolos en sistemas como Linux, SolarisTM, WindowsTM, HP-UXTM.

Para la utilización de GModule es necesario tener presente dos cosas. La primera es que, en el momento de la compilación, se debe indicar que se va a usar GModule, y para ello se indicará en el pkg-config que incluya la biblioteca gmodule-2.0. Y por supuesto incluir la linea:

#include <gmodule.h>

La otra tiene que ver con detectar si el sistema operativo en el que se trabaja, soporta este tipo de acciones. Y para ello se usará la función g_module_supported que detectará si se puede hacer uso de las siguientes funciones y devolverá TRUE si es así.

gboolean g_module_support ( void );

Abrir una biblioteca.

Si el sistema en el que se trabaja soporta el uso de GModule, el siguiente paso será abrir la biblioteca de la cual posteriormente se extraerá el símbolo deseado. Para ello se debe indicar de alguna manera la posición en nuestro sistema de la biblioteca (plugin) que se desea abrir. Y con este fin, se usara la función g_module_build_path, con la que podremos construir la ruta exacta donde se encuentra la misma. Esta función devolverá una cadena con la ruta anteriormente nombrada.

gchar * g_module_build_path (const gchar * directorio,
                             const gchar * nombre_del_modulo );

En el primer parámetro de esta función se debe especificar el directorio donde se encuentra la biblioteca que se desea abrir. Y en el segundo, el nombre de la misma. Pero hay que tener claro que sólo el nombre, ya que la función se encargará de poner la extensión de la biblioteca según el sistema operativo en el que este corriendo GLib. Por ejemplo, en Linux colocará la extensión ".so" y en WindowsTM ".dll".

Una vez que se obtiene la ruta devuelta por g_module_build_path, lo siguiente será cargar la biblioteca. Y para ello está la siguiente función.

GModule * g_module_open (const gchar * ruta_de_la_libreria,
                         GModuleFlags bandera );

El parámetro bandera puede contener dos valores, un cero o la macro G_MODULE_BIND_LAZY. El primer valor, el cero, significa que, cuando se ejecuta esta función, se cargan todos los símbolos de la biblioteca. Mientras que con el segundo valor, sólo lo hará cuando sea necesario.

Otra cosa a tener en cuenta es el resultado de esta función. Como se puede observar, devuelve un puntero a una estructura GModule. Esta estructura es opaca a la visión del desarrollador, pero servirá en posteriores funciones como referencia a la biblioteca que hemos abierto. En el caso de que GModule sea igual a NULL, ésto significará que la función no lo ha conseguido.

Obtener un símbolo determinado.

Ahora ha llegado el momento de obtener el símbolo que se desee de la biblioteca. Como se dijo antes, estos símbolos pueden ser funciones, estructuras o variables que hayan sido exportadas en la biblioteca (plugin), así que, de momento, se dará por hecho que existe el símbolo al que se va a llamar con la siguiente función.

gboolean g_module_symbol (GModule * modulo, const gchar * símbolo,
                          gpointer * puntero_al_simbolo );

Esta función recibirá como parámetros el puntero a la estructura GModule, que hace referencia a la biblioteca que anteriormente se ha abierto. Después se introduce el nombre del símbolo que se desea obtener y, para terminar, la función devolverá en su tercer parámetro un puntero al símbolo que se haya solicitado. En el caso que se haya solicitado una función, devolverá un puntero a esa función. Y si lo que se ha solicitado es una variable devolverá un puntero a la misma. Para terminar sólo queda decir que el valor boolean que devuelve, verifica si la función se ha cumplido con éxito o no.

Preparar el plugin.

Con las funciones anteriores se ha clarificado el trabajo que se ha de realizar para llamar al símbolo de la biblioteca (plugin). Ahora queda tratar la parte contraria, la del plugin. Se ha de tener en cuenta que para que la función g_module_symbol encuentre el símbolo que se reclame, este ha de estar declarado de alguna manera. Y para este acto se usa la macro G_MODULE_EXPORT. Esta macro, escrita delante de una variable o función, realiza la función que se desea, la de exportar el símbolo. De este modo, ya será posible referenciar el símbolo deseado desde la función g_module_symbol.

Dentro de la preparación de plugin con GModule existe una manera de inicializar el plugin. Para ello se deberá declarar una función llamada g_module_check_init. Dentro de esa función, es posible declarar acciones para la inicialización del plugin. Pero esta función deberá seguir estrictamente este prototipo.

const gchar* (*GModuleCheckInit) (GModule * modulo );

En el momento en el que se abra la biblioteca (plugin) con la función g_module_open, ésta buscará esta función y la ejecutará. En el caso que esta función no esté declarada, simplemente habrá abierto la biblioteca. En el caso de que exista la función, podrá retornar NULL si ha sido exitosa la inicialización o un cadena de caracteres con la información del error contenida en ella.

Y al igual que se puede especificar una manera de inicializar el plugin, también se puede especificar la manera en la que finalizará. Para ello se deberá especificar una función llamada g_module_unload. Y, al igual que la anterior, deberá seguir el siguiente prototipo.

void (*GModuleUnload) (GModule * modulo );

Ejemplo de uso de GModule.

Llegados a este punto, es hora demostrar prácticamente todo lo anteriormente explicado. Para ello se usará el siguiente ejemplo, que consta de cuatro archivos. Uno de ellos es un Makefile, que nos ayudará a compilar dicho ejemplo y hará más familiar el uso de la herramienta make.

El siguiente ejemplo explicará cómo funciona un plugin. Para ello se cuenta, por un lado, con una aplicación, correspondiente a los ficheros modulo.c y modulo.h. Esta aplicación llamará al plugin generado por el fichero plugin.c, que cuando lo compilemos se llamará libplugin.so. La aplicación exportará una estructura llamada info_plugin y una función llamada escribe. Dentro de la aplicación, se llamará a los símbolos anteriormente citados y se modificará el contenido de la estructura. Una vez ese contenido ha sido modificado, se llamará al puntero a función correspondiente al símbolo de la función escribe. Cuando esta función se ejecuta dentro de la aplicación, lo que realmente se esta ejecutando es la función escribe, que imprime por pantalla el contenido de la estructura.

Ejemplo 6-11.1. Ejemplo de Makefile para usar GModule

# Este es un ejemplo del archivo Makefile.
#
# Para proyectos de mayor complejidad es 
# recomendable la utilizacion de autotools.

CC = gcc
CFLAGS = -ggdb -O0
ERROR_CFLAGS = -Wall -Werror
GLIB_CFLAGS = `pkg-config --cflags --libs glib-2.0 gmodule-2.0`

all:
        $(CC) $(CFLAGS) $(ERROR_CFLAGS) $(GLIB_CFLAGS) -o modulo modulo.c
        $(CC) -shared $(CFLAGS) $(ERROR_CFLAGS) $(GLIB_CFLAGS) -o libplugin.so plugin.c

clean:
        rm -rf libplugin.so modulo

Ejemplo 6-11.2. Cabecera del Módulo

   1 /* ejemplo de cabecera llamada modulo.h */
   2 #ifndef __MODULO_H__
   3 #define __MODULO_H__
   4 
   5 #include <glib.h>
   6 #include <gmodule.h>
   7 
   8 typedef struct _datos_plugin datos_plugin;
   9 
  10 struct _datos_plugin {
  11            gchar *nombre;
  12            gchar *correo;
  13 };
  14 
  15 
  16 #endif /* __MODULO_H__ */
  17 

Ejemplo 6-11.3. Código del Módulo

   1 /* ejemplo de programa modulo.c */
   2 #include <stdlib.h>
   3 #include "modulo.h"
   4 
   5 /*
   6 *       Se define un tipo, puntero a función nula, para
   7 *       después declarar una variable (escribe), a la que
   8 *       se asignará un símbolo(en este caso una función) del plugin.
   9 *
  10 */
  11 typedef void (*funcion_escritura) (void);
  12 
  13 int
  14 main (int argc, char **argv)
  15 {
  16         gchar *dir;
  17         GModule *modulo;
  18         gchar *plugin;
  19         datos_plugin **info;
  20         funcion_escritura escribe; /*  Variable que apuntará al símbolo */
  21 
  22         /*  Se comprueba que el sistema soporta los plugins */
  23         if (!g_module_supported ())
  24                 return 0;
  25 
  26         dir = g_get_current_dir ();
  27 
  28         /*  Se obtiene el path del plugin que se usará */
  29         plugin = g_module_build_path (dir, "libplugin");
  30 
  31         g_free (dir);
  32 
  33         /*  Se carga el plugin  */
  34         modulo = g_module_open (plugin, G_MODULE_BIND_LAZY);
  35         if (!modulo)
  36                 g_error ("error: %s", g_module_error ());
  37 
  38         /* Se obtiene el símbolo info_plugin, una estructura de datos */
  39         if (!g_module_symbol (modulo, "info_plugin", (gpointer *) & info))
  40                 g_error ("error: %s", g_module_error ());
  41 
  42         (*info)->nombre = "Pepe Lopez";         /*   Aquí se usa el símbolo  */
  43         (*info)->correo = "pepe@gnome.org";     /*   info_plugin             */
  44 
  45         /* Se obtiene el símbolo plugin_escribe, una función */
  46         if (!g_module_symbol
  47             (modulo, "plugin_escribe", (gpointer *) & escribe))
  48                 g_error ("error: %s", g_module_error ());
  49 
  50         escribe ();             /*      Aquí se usa el símbolo plugin_escribe */
  51 
  52         /*      Se descarga el plugin   */
  53         if (!g_module_close (modulo))
  54                 g_error ("error: %s", g_module_error ());
  55 
  56         return EXIT_SUCCESS;
  57 }

Ejemplo 6-11.4. Ejemplo de plugin

   1 /* ejemplo de plugin.c */
   2 #include "modulo.h"
   3 
   4 /*
   5 *       Se declara un puntero a una estructura de datos
   6 *       y se exporta como símbolo, para poder ser
   7 *       utilizado por las aplicaciones que carguen
   8 *       este plugin
   9 *
  10 */
  11 G_MODULE_EXPORT datos_plugin *info_plugin;
  12 
  13 
  14 /*
  15 *       Esta función inicializa el plugin. Se exporta como simbolo.
  16 *
  17 *       Se ejecuta al ser cargado el plugin -> g_module_open()
  18 *
  19 */
  20 G_MODULE_EXPORT const gchar *
  21 g_module_check_init (GModule * module)
  22 {
  23         g_print ("--- Inicializacion del plugin ---\n\n");
  24         info_plugin = g_malloc (sizeof (datos_plugin));
  25         return NULL;
  26 }
  27 
  28 
  29 /*
  30 *       Esta función finaliza el plugin. Se exporta como simbolo.
  31 *
  32 *       Se ejecuta al ser descargado el plugin -> g_module_close()
  33 *
  34 */
  35 G_MODULE_EXPORT void
  36 g_module_unload (GModule * module)
  37 {
  38         g_print ("\n--- Finalizacion del plugin ---\n");
  39         g_free (info_plugin);
  40 }
  41 
  42 
  43 /*
  44 *       Esta función será un símbolo del plugin
  45 *       que se podrá llamar a voluntad en cualquier
  46 *       momento, una vez se haya cargado el plugin.
  47 *
  48 */
  49 G_MODULE_EXPORT void
  50 plugin_escribe (void)
  51 {
  52         g_return_if_fail (info_plugin != NULL && info_plugin->nombre != NULL &&
  53                 info_plugin->correo != NULL);
  54 
  55         g_print ("Nombre : %s \ncorreo-e : %s\n",
  56                  info_plugin->nombre, info_plugin->correo);
  57 }

Y el resultado de ejecutar el programa obtenido de compilar, tanto el plugin, como el modulo con make, sería el siguiente:

          bash$:/var/svn/librognome/book/code/GModule$
          
          ./modulo
          --- Inicializacion del plugin ---

              Nombre : Pepe Lopez
              correo-e : pepe@gnome.org

          --- Finalizacion del plugin ---

Documentacion/Desarrollo/Glib (última edición 2008-12-04 08:48:53 efectuada por localhost)