Charlas de GNOME Hispano: Glib avanzado

Relator: Rodrigo Moya (rodrigo)
Moderador: Germán Poo (gpoo)
Domingo 22 de diciembre de 2002, 21:30 horas UTC
irc.gnome.cl #gnome-hispano

gpoo
Estamos listos para comenzar una nueva charla
este domingo será sobre GLib: nivel avanzado
en donde se verá el gestor de errores
threads (hilos) y plug-ins (extensiones)
a cargo de Rodrigo Moya
les recuerdo
las consultas las canalizan a través de mí
rodrigo, cuando quieras
rodrigo
bien
hola
lo primero, decirle a gpoo que me puede cortar con preguntas cuando quiera

Gestión de errores
en las charlas anteriores, creo recordar que se comentó
el uso de GError para recibir información d eerrores de las funciones de
 glib
hay algunas funciones (no todas) 
que reciben un parámetro de tipo:
GError **ret_error
que es un puntero a un puntero a GError
por ejemplo:
GDir    *               g_dir_open      (const gchar  *path,
                                         guint         flags,
                                         GError      **error);
(g_dir_open se usa para abrir un directorio, para obtener una lista de
 los ficheros contenidos en él)
el parámetro "error" es como os comentaba antes
la forma de usar esta (y todas las funciones que usen este método) es muy
 sencilla:
GError *error = NULL;
dir = g_dir_open ("/home/rodrigo", 0, &error)
se le pasa un puntero a un puntero a GError que hemos inicializado a NULL

al retornar, g_dir_open habrá rellenado el parámetro "error"
con la información del error producido, si es que se produjo alguno.
Es decir:
...llamada a g_dir_open...
if (error) {
  // Se ha producido un error
si no se produce ningun error, el parámetro "error" seguirá siendo NULL

cuando se produce un error
lo que hace g_dir_open (y demás funciones que usan este método)
es crear una instancia de la estructura GError
definida en gerror.h:
struct _GError
{
  GQuark       domain;
  gint         code;
  gchar       *message;
};
ahora explicaremos cómo crear nuevos GError para usar este método en nuestras
 apps
pero en cuanto al uso de este método de obtención de información de errores

todo lo que hay qye saber es lo mencionado antes:
GError *error = NULL;
/ llamada a la función con ....&error)
if (error)
  // se ha producido un error
simplemente añadir a esto que, cuando "error" no sea NULL
podemos mostrar mensajes de error más concretos que "ha habido un error"
 o cosas por el estilo
podemos obtener el mensaje de error y su código:
if (error) {
  g_print ("Error %d: %s\n", error->code, error->message);
para terminar, decir que una vez que no necesitemos más el GError retornado

debemos librerarlo, puesto que la función que nos devuelve el error
creó una instancia de GError
por tanto:
if (error) {
  g_print ("Error %d: %s\n", error->code, error->message);
  g_error_free (error);
}
ahora vamos a ver cómo usar esto en nuestras aplicaciones
imaginemos que tenemos una función que hace una operación (cualquiera)

void hacer_operacion (tipo1 param1, tipo2 param2, ... GError **error)
{
  // hacer lo que sea..
  if (se_ha_producido_un_error) {
  *error = g_error_new (g_quark_from_static_string ("Mi aplicación"), codigo_de_error,
 "Mensaje de error");
}
esta función hace lo mismo que g_dir_open
si se produce un error, crea un GError con la info referente al error producido

para crear errores, hay dos funciones:
GError*     g_error_new                     (GQuark domain,
                                             gint code,
                                             const gchar *format,
                                             ...);
GError*     g_error_new_literal             (GQuark domain,
                                             gint code,
                                             const gchar *message);
ambas son similares
se diferencian en que una (g_error_new) permite el uso de un formato igual
 que el de printf:
error = g_error_new (g_quark_from_static_string ("Mi aplicación"), codigo_de_error,
 "Error %d producido en hacer_operacion", codigo_de_error);
mientras que la otra (g_error_new_literal) acepta una única cadena de texto
 que represernta el mensaje de error
el primero parámetro de ambas es de tipo GQuark
que es otro tipo de datos que se usa para asociar enteros->cadenas
esto se usa para almacenar listas de cadenas (constantes) normalmente
en este caso, lo que se hace es asociar, a cada GError
un dominio de errores, que es, básicamente, el nombre de la aplicación

puesto que un GQuark es una asociación entero->cadena
simplemebte creamos un nuevo GQuark a partir de una cadena constante
que en este caso, es el nombre de la aplicación "Mi aplicación"
tambien, en el caso de librerías, se usa el nombre de la librería como
 dominio
esto es importante, puies nos permite diferenciar dónde se produjo el error

hay distintas funciones para la gestión de GError's:
void     g_error_free          (GError        *error);
GError*  g_error_copy          (const GError  *error);
                                                                      
                         
gboolean g_error_matches       (const GError  *error,
                                GQuark         domain,
                                gint           code);
void     g_set_error           (GError       **err,
                                GQuark         domain,
                                gint           code,
                                const gchar   *format,
                                ...) G_GNUC_PRINTF (4, 5);
void     g_clear_error         (GError       **err);
creo que no necesitan mucha explicación
así que, si alguien no las entiende que pregunte ahora o calle para siempre
 :-)
--- gnomero fija modos [#gnome-hispano +o slack]
--- slack fija modos [#gnome-hispano -o slack]
gpoo
al parecer se entiende todo
rodrigo
bien
pues seguimos con hilos
Hilos (threads)
gpoo
rodrigo, antes de continuar
el codigo_de_error es un numero arbitrario, cierto?
rodrigo
en principio, si
la cosa es usar algo que te permita identificar el error
puesto que aun no hay una lista de códigos en GLib
cada aplicación usará el que quiera
gpoo
basicamente enumeraciones
rodrigo
si
gpoo
ok, todo claro
rodrigo
supongo que lo ideal es usar los códigos de erorr de la libc
bien
Hilos (threads)
Supongo que todos sabeis que las apps normalmente usan un único hilo de
 ejecución
esto quiere decir que la aplicación se ejecuta secuencialmente
y si una parte se queda bloqueada (esperando por E/S , etc)
toda la app se queda bloqueada
por supuesto, hay otros métodos para solventar esto
de hecho, yo no soy muy partidario del uso de threads
salvo en casos en los que su uso es la única solución (o la menos mala)

por ejemplo, la E/S se puede hacer no bloqueante de forma muy sencilla

los threads permiten que una aplicación tenga varios hilos de ejecución
 conucrrentes
por supuesto, una sola instrucción (en máquinas con 1 procesador) será
 ejecuta cada vez
pero el programa mantiene varios hilos concurrentes
de forma que si uno se queda bloqueado, el resto de hilos siguen su ejecución

el añadir varios hilos de ejecución suena maravilloso a primera vista
porque parece que abre muchas posibilidades
pero, evidentemente, tambien conlleva nuevos problemas
especialmente el acceso concurrente a variables del programa
que es el mayor quebradero de cabeza al usar threads
no pretendo desanimar a nadie para que no use threads
simplemente aviso de sus problemas, y de mi opinión sobre ellos
pues no quiero que nadie salga de aquí pensando que sion LA solución
cuando no es cierto
gpoo
-ekeko- Ximian Evolution es un ejemplo de múltiples hilos?
rodrigo
son la solución a algunos problemas, pero no a todos :-)
ekeko: la parte del mailer si usa múltiples hilos
ekeko: uno para el UI, y 'n' para las operaciones no-UI
el resto de componentes no
por cierto, ya que mencionas evolution
muchas veces, al hacer un ps se ven cosas como:
$ ps ax | grep evolution-mail
  782 ?        S      1:43 evolution-mail --oaf-activate-iid=OAFIID:GNOME_Evolution_Mail_ShellComponent
 --oaf-ior-fd=16
  849 ?        S      0:00 evolution-mail --oaf-activate-iid=OAFIID:GNOME_Evolution_Mail_ShellComponent
 --oaf-ior-fd=16
  850 ?        S      0:10 evolution-mail --oaf-activate-iid=OAFIID:GNOME_Evolution_Mail_ShellComponent
 --oaf-ior-fd=16
  851 ?        S      0:07 evolution-mail --oaf-activate-iid=OAFIID:GNOME_Evolution_Mail_ShellComponent
 --oaf-ior-fd=16
  852 ?        S      0:12 evolution-mail --oaf-activate-iid=OAFIID:GNOME_Evolution_Mail_ShellComponent
 --oaf-ior-fd=16
  854 ?        S      0:31 evolution-mail --oaf-activate-iid=OAFIID:GNOME_Evolution_Mail_ShellComponent
 --oaf-ior-fd=16
  857 ?        S      0:11 evolution-mail --oaf-activate-iid=OAFIID:GNOME_Evolution_Mail_ShellComponent
 --oaf-ior-fd=16
  858 ?        S      0:04 evolution-mail --oaf-activate-iid=OAFIID:GNOME_Evolution_Mail_ShellComponent
 --oaf-ior-fd=16
  912 ?        S      1:42 evolution-mail --oaf-activate-iid=OAFIID:GNOME_Evolution_Mail_ShellComponent
 --oaf-ior-fd=16
18772 ?        S      0:01 evolution-mail --oaf-activate-iid=OAFIID:GNOME_Evolution_Mail_ShellComponent
 --oaf-ior-fd=16
parece que hay 'n' copias de evolution-mail
cuando realmente hay una con múltiples hilos
bueno, para usar hilos en GLib, lo primero que hay que hacer es:
g_thread_init (NULL);
esto inicializa el soporte de threads en GLib
esta función sólo puede ser llamada una vez
así que, para asegurarnos de que así sea:
if (!g_thread_supported ()) g_thread_init (NULL);
esta función aborta el programa si no hay soporte de threadsa en el sistema

por cierto, las funciones de g_threas estan en la librería libgthread-2.0,
 no libglib2.0
por tanto, para usar estas funciones, tenemos que compilar con:
gcc .... `pkg-config --libs gthread-2.0`
para añadir la librería adicional a nuestro programa
una vez que está inicializado el sistema de threads
se pueden crear nuevos threads, con:
GThread*    g_thread_create                 (GThreadFunc func,
                                             gpointer data,
                                             gboolean joinable,
                                             GError **error);
esta función devuelve un nuevo GThread
despues de realmente inicializar un nuevo hilo de ejecución en el que se
 hace una llamada
a la función especificada en el parámetro 'func'
que es de tipo GTHreadFunc:
gpointer    (*GThreadFunc)                  (gpointer data);
el parámetro 'data' en g_thread_create es el que luego es pasado a "func"

joinable especifica si deseamos poder esperar a que el thread termine (luego
 lo vemos)
mientras que "error", como ya habeis adivinado, se usa para recibir info
 del error que se produzca
un ejemplo:
GThread *th;
GError *error = NULL;
th = g_thread_create ((GThreadFunc) mi_thread, NULL, TRUE, &error);

if (error) {
  g_print ("Error: %s\n", error->message);
  g_error_free (error);
}
la función mi_thread sería:
gpointer
mi_thread (gpointer user_data)
{
  // hacer lo que sea
  return valor_de_retorno_del_thread);
}
una vez que se salga de la función mi_thread
el thread es terminado inmediatamente
el valor_de_retorno_del_thread
se usa para poder enviar de vuelta información del estado de finalización
 del thread
para obtener esta info de finalización del thread
1) debemos crear los threads con 'joinable' = TRUE
2) debemos usar, desde otras partes de la app, la función g_thread_join

esta función (g_thread_join) permite esperar a que un thread termine y
 obtener dicha info
dicha 'info' es un gpointer, como podeis ver, de forma que es completamente
 específico de la app que estemos desarrollando
puede ser un puntero a un código de error (entero), un GError, o cualquier
 otra cosa que queramos
para terminar threads, como he comentado,
se sale de la fucnión especificada en la llamada a g_thread_create (mi_thread)

pero tambien pueden ser terminados, normalmente desde fuera del propio
 thread (fuera de mi_thread),
usando la función g_thread_exit
perdón, esta función sólo puede ser llamada desde el propio thread :-)

void        g_thread_exit                   (gpointer retval);
o sea, sólo desde dentro de mi_thread
tambien podemos poner prioridades a los threads
de forma que unos se ejecuten antes que otros:
void        g_thread_set_priority           (GThread *thread,
                                             GThreadPriority priority);

donde:
typedef enum
{
  G_THREAD_PRIORITY_LOW,
  G_THREAD_PRIORITY_NORMAL,
  G_THREAD_PRIORITY_HIGH,
  G_THREAD_PRIORITY_URGENT
} GThreadPriority;
dentro de mi_thread, podemos obtener fácilmente una referencia al GThread
 creado para ese hilo en concreto
con:
GThread*    g_thread_self                   (void);
así, por ejemplo:
gpointer mi_thread (gpointer user_data)
{
  g_thread_set_priority (g_thread_self (), G_THREAD_PRIORITY_URGENT);
tambien podemos ceder tiempo de proceso a otros threads:
void g_thread_yield (void)
desde fuera de los threads, para esperar a que termine:
gpointer valor_de_retorno_del_thread;
valor_de_retorno_del_thread = g_thread_join (th);
en valor_de_retorno_del_thread obtendremos el valor de retorno devuelto
 por mi_thread
para la gestión del acceso a datos por múltiples hilos, se usan los GMutex

(de Mutual Exclusion)
que es una forma de bloquear el acceso a datos del programa
popr ello,
lo mejor, si queremos acceso concurrente a datos compartidos por toda la
 aplicación
(ojo, compartidos no me refiero a variables globales, si no a posiciones
 de memoria a las que todos acceden)
debemos crear un GMutex:
GMutex*     g_mutex_new                     ();
esto simplemente crea un nuevo GMutex
cuando queramos acceder a los datos compartidos:
GMutex *mutex = g_mutex_new ();
g_mutex_lock (mutex);
g_mutex_lock bloquea el acceso al GMutex
pero lo interesante de esta función es que, al llamarla desde dentro de
 un hilo,
si el GMutex ya está bloqueado por otro hilo,
esta función no retorna hasta que el otro hilo desbloquee el GMutex
es decir:
gpointer mi_thread (gpointer user_data)
{
  g_mutex_lock (mutex_global);
  ... procesar datos compartidos
  g_mutex_unlock (mutex);
...
por supuesto, es muy mala idea bloquear el mutex al principio del thread
 y no desbloquearlo hasta el final
esto significaría que el mutex en cuestión no permitiría el acceso a los
 datos a otros hilos hasta terminar
por ello, hay que poner especial atención en sólo bloquear cuando realmente
 se vaya a hacer el acceso:
while (lo:_qu
while (lo_que_sea) {
  .. obtenemos datos de una fuente externa, por ejemplo
  ...
  // ahora, actualizamos los datos compartidos
  g_mutex_lock (mutex_global);
  ...
   g_mutex_unlock (mutex_global);
...
}
en vez de:
g_mutex_lock (mutex_global);
while ...
{ ... }
g_mutex_unlock (mutex_global);
creo que se ve la diferencia de lo que quiero decir ¿no?
alguna pregunta sobre threads?
gpoo
al parecer no, a mi me ha parecido todo muy claro
rodrigo
ok
gpoo
<jmx> Habria que liberar el GMutex?
rodrigo
si, g_mutex_free
gpoo
adelante rodrigo
rodrigo
Módulos (o plugins)
los módulos (o plugins) permiten, normalmente, extender una aplicación

son simples librerías compartidas,
que podemos cargar desde nuestras apps
seguro que muchos ya habeis usado dlopen, dlclose...
que son funciones para hacer eso
de unix
glib, como con otras cosas, ofrece una forma portable de todas estas cosas
 comunes de todos los sistemas
al igual que gthread, gmodule es una lib extra
por tanto, hay que usar pkg-config --libs gmodule-2.0 para enlazar con
 ella
antes de usar las funciones de gmodule, debemos saber si el sistema
en el que se ejecuta la app lo soporta
(GLib tiene soporte para todos los unix con dlopen, para HP-UX via shl_load,
 y para windows con sus .dll
por tanto, antes de cargar ningun módulo:
if (!g_module_supported ())
  g_error ("No hay soporte para GModule\n");
una vez comprobada la existencia del soporte
podemos cargar nuevos módulos con g_module_open:
GModule*    g_module_open                   (const gchar *file_name,
                                             GModuleFlags flags);
el primer parámetro es el fichero (.so) que representa el módulo a cargar

puesto que el nombre de esos ficheros es dependiente del sistema (.so/.dll...)

podemos usar la función:
gchar*      g_module_build_path             (const gchar *directory,
                                             const gchar *module_name);

así:
gchar *path = g_module_build_path (directorio_de_plugins, "plugin");
GModule *module = g_module_open (path, 0);
g_free (path);
g_module_build_path construye el nombre de la librería a cargar
en el caso de linux, la llamada anterior devolvería
suponiendo que directorio_de_plugins = /usr/lib/plugins, por ejemplo:
/usr/lib/plugins/libplugin.so
en el caso de windows: ...\plugin.dll
...
con g_module_open no solo podemos cargar librerías
si no que tambien podemos cargar un ejecutable y hacer llamadas a sus funciones

normalmente, usaremos librerías, que es lo más comun
uyna vez obtenido, con g_module_open un GModule válido
sólo nos queda obtener punteros a las funciones/datos del módulo
eso se hace con:
gboolean    g_module_symbol                 (GModule *module,
                                             const gchar *symbol_name,

                                             gpointer *symbol);
por ejemplo, para cargar la función gtk_button_new_with_label de libgtk-2.0.so:

GModule *module:
module = g_module_open ("/usr/lib/libgtk-2.0.so", 0);
if (!module)
  g_error ("No se pudo cargar gtk-2.0");
typedef GtkWidget * (* ProtoGtkButtonNewWithLabel) (const gchar *label);

ProtoGtkButtonNewWithLabel func_new_button_with_label:
g_module_symbol (module, "gtk_button_new_with_label", &func_new_button_with_label);

Gtkwidget *boton = (* func_new_button_with_label) ("Hola");
lo que hacemos es declarar un tipo de datos que es un puntero a función

con la misma firma (ProtoGtkButtonNewWithLabel) que la función a cargar

y pasamos un puntero a una variable de ese tipo
donde obtenemos el puntero a la función carga desde el módulo
Gtkwidget *boton = (* func_new_button_with_label) ("Hola");
en esta línea, llamamos a la función cargada desde el módulo
cuya dirección ha sido almacenada en func_new_button_with_label
para cargar variables desde el módlo
el mecanismo es el mismo
pero en cambio de usar punteros a funciones, usamos punteros a estructuras
 you otros tipos de datos básicos
por ejemplo:
gint entero_cargado_desde_el_modulo;
g_module_symbol (module, "entero_declarado_en_el_modulo", &entero_cargado_desde_el_modulo);

entero_cargado_desde_el_modulo = 37;
perdón:
gint *entero_cargado_desde_el_modulo;
g_module_symbol (module, "entero_declarado_en_el_modulo", &entero_cargado_desde_el_modulo);

*entero_cargado_desde_el_modulo = 37;
una vez que no se necesite más el módulo
se cierra con:
g_module_close (module);
una vez que se haga esto, los símbolos (funciones, variables) cargados
 desde el módulo
serán inválidos, y por tanto no se podrán usar más
se puede hacer que un módulo permanezca suempre en memoria:
void        g_module_make_resident          (GModule *module);
esta función hace que las llamadas a g_module_close sobre el módulo hecho
 "residente" sean ignoradas
y por tanto el módulo permanece en memoria durante todo el programa
es interesante que gmodule, cuando carga un módulo
busca unas funciones:
const gchar *g_module_check_init (GModule *module);
void g_module_unload (GModule *module)
si estas funciones estan presentes en el .so que se carga con g_module_open

g_module_check_init será llamada cuando se cargue el módulo (g_module_open)

y g_module_unload será llamada cuando se descargue (g_module_close)
y esto es todo
preguntas?
gpoo
<jmx> No veo claro lo de uload despues de close
rodrigo
jmx: g_module_unload se llama antes de descargar completamente el módulo

o sea, el módulo sigue estando en memoria cuando _unload es llamada
g_module_unload no es una función de libgmodule
si no una función presente en los .so
si esas funciones existen en el .so, son llamadas desde libgmodule
si no, no se hace ninguna llamada desde g_module_open/_close
gpoo
<jmx> Cuando cargamos un modulo lo hacemos en el espacio de trabajo
 de nuestra aplicacion (no se comparten con otras aplicaciones)
rodrigo
jmx: si, lo cargas desde el segmento de memoria de tu aplicación
es decir, desde otros procesos, no se puede acceder a las funciones que
 tu has cargado
pero si pueden acceder, a través de GModule, al módulo
eso si, sería una copia nueva del módulo